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.
- common/__init__.py +0 -0
- common/core/__init__.py +6 -0
- common/core/app.py +6 -0
- common/core/cli/__init__.py +0 -0
- common/core/cli/healthcheck.py +120 -0
- common/core/logging.py +24 -0
- common/core/main.py +105 -0
- common/core/management/__init__.py +0 -0
- common/core/management/commands/__init__.py +0 -0
- common/core/management/commands/docgen.py +63 -0
- common/core/management/commands/start.py +61 -0
- common/core/management/commands/waitfordb.py +87 -0
- common/core/metrics.py +25 -0
- common/core/middleware.py +22 -0
- common/core/templates/docgen-metrics.md +22 -0
- common/core/urls.py +17 -0
- common/core/utils.py +239 -0
- common/core/views.py +27 -0
- common/environments/permissions.py +15 -0
- common/features/__init__.py +0 -0
- common/features/multivariate/__init__.py +0 -0
- common/features/multivariate/serializers.py +19 -0
- common/features/serializers.py +68 -0
- common/features/versioning/__init__.py +0 -0
- common/features/versioning/serializers.py +13 -0
- common/gunicorn/__init__.py +0 -0
- common/gunicorn/conf.py +18 -0
- common/gunicorn/constants.py +23 -0
- common/gunicorn/logging.py +120 -0
- common/gunicorn/metrics.py +26 -0
- common/gunicorn/middleware.py +30 -0
- common/gunicorn/utils.py +104 -0
- common/migrations/__init__.py +0 -0
- common/migrations/helpers/__init__.py +9 -0
- common/migrations/helpers/postgres_helpers.py +41 -0
- common/organisations/permissions.py +10 -0
- common/projects/permissions.py +40 -0
- common/prometheus/__init__.py +3 -0
- common/prometheus/utils.py +38 -0
- common/py.typed +0 -0
- common/test_tools/__init__.py +11 -0
- common/test_tools/plugin.py +139 -0
- common/test_tools/types.py +56 -0
- common/test_tools/utils.py +11 -0
- common/types.py +45 -0
- flagsmith_common-2.2.4.dist-info/METADATA +196 -0
- flagsmith_common-2.2.4.dist-info/RECORD +92 -0
- flagsmith_common-2.2.4.dist-info/WHEEL +4 -0
- flagsmith_common-2.2.4.dist-info/entry_points.txt +6 -0
- flagsmith_common-2.2.4.dist-info/licenses/LICENSE +28 -0
- task_processor/__init__.py +0 -0
- task_processor/admin.py +38 -0
- task_processor/apps.py +47 -0
- task_processor/decorators.py +209 -0
- task_processor/exceptions.py +28 -0
- task_processor/health.py +44 -0
- task_processor/managers.py +18 -0
- task_processor/metrics.py +22 -0
- task_processor/migrations/0001_initial.py +44 -0
- task_processor/migrations/0002_healthcheckmodel.py +21 -0
- task_processor/migrations/0003_add_completed_to_task.py +22 -0
- task_processor/migrations/0004_recreate_task_indexes.py +43 -0
- task_processor/migrations/0005_update_conditional_index_conditions.py +45 -0
- task_processor/migrations/0006_auto_20230221_0802.py +45 -0
- task_processor/migrations/0007_add_is_locked.py +23 -0
- task_processor/migrations/0008_add_get_task_to_process_function.py +31 -0
- task_processor/migrations/0009_add_recurring_task_run_first_run_at.py +18 -0
- task_processor/migrations/0010_task_priority.py +27 -0
- task_processor/migrations/0011_add_priority_to_get_tasks_to_process.py +27 -0
- task_processor/migrations/0012_add_locked_at_and_timeout.py +40 -0
- task_processor/migrations/0013_add_last_picked_at.py +34 -0
- task_processor/migrations/__init__.py +0 -0
- task_processor/migrations/sql/0008_get_recurring_tasks_to_process.sql +30 -0
- task_processor/migrations/sql/0008_get_tasks_to_process.sql +30 -0
- task_processor/migrations/sql/0011_get_tasks_to_process.sql +30 -0
- task_processor/migrations/sql/0012_get_recurringtasks_to_process.sql +33 -0
- task_processor/migrations/sql/0013_get_recurringtasks_to_process.sql +33 -0
- task_processor/migrations/sql/__init__.py +0 -0
- task_processor/models.py +237 -0
- task_processor/monitoring.py +12 -0
- task_processor/processor.py +202 -0
- task_processor/py.typed +0 -0
- task_processor/routers.py +55 -0
- task_processor/serializers.py +7 -0
- task_processor/task_registry.py +90 -0
- task_processor/task_run_method.py +7 -0
- task_processor/tasks.py +71 -0
- task_processor/threads.py +128 -0
- task_processor/types.py +18 -0
- task_processor/urls.py +5 -0
- task_processor/utils.py +71 -0
- task_processor/views.py +20 -0
common/__init__.py
ADDED
|
File without changes
|
common/core/__init__.py
ADDED
common/core/app.py
ADDED
|
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)]
|