flagsmith-common 3.3.0__tar.gz → 3.5.0__tar.gz
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.
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/PKG-INFO +24 -1
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/README.md +20 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/pyproject.toml +6 -1
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/core/constants.py +2 -0
- flagsmith_common-3.5.0/src/common/core/logging.py +181 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/core/main.py +20 -3
- flagsmith_common-3.5.0/src/common/core/sentry.py +22 -0
- flagsmith_common-3.5.0/src/common/gunicorn/logging.py +66 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/gunicorn/metrics.py +1 -6
- flagsmith_common-3.5.0/src/common/gunicorn/processors.py +74 -0
- flagsmith_common-3.5.0/src/common/lint_tests.py +279 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/prometheus/utils.py +1 -4
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/flagsmith_schemas/dynamodb.py +7 -1
- flagsmith_common-3.3.0/src/common/core/logging.py +0 -24
- flagsmith_common-3.3.0/src/common/gunicorn/logging.py +0 -120
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/LICENSE +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/core/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/core/app.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/core/cli/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/core/cli/healthcheck.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/core/management/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/core/management/commands/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/core/management/commands/docgen.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/core/management/commands/start.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/core/management/commands/waitfordb.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/core/metrics.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/core/middleware.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/core/templates/docgen-metrics.md +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/core/urls.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/core/utils.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/core/views.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/environments/permissions.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/features/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/features/multivariate/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/features/multivariate/serializers.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/features/serializers.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/features/versioning/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/features/versioning/serializers.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/gunicorn/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/gunicorn/conf.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/gunicorn/constants.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/gunicorn/metrics_server.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/gunicorn/middleware.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/gunicorn/utils.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/migrations/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/migrations/helpers/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/migrations/helpers/postgres_helpers.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/organisations/permissions.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/projects/permissions.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/prometheus/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/py.typed +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/test_tools/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/test_tools/plugin.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/test_tools/types.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/test_tools/utils.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/common/types.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/flagsmith_schemas/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/flagsmith_schemas/api.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/flagsmith_schemas/constants.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/flagsmith_schemas/py.typed +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/flagsmith_schemas/pydantic_types.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/flagsmith_schemas/types.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/flagsmith_schemas/utils.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/flagsmith_schemas/validators.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/admin.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/apps.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/decorators.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/exceptions.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/health.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/managers.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/metrics.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/migrations/0001_initial.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/migrations/0002_healthcheckmodel.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/migrations/0003_add_completed_to_task.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/migrations/0004_recreate_task_indexes.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/migrations/0005_update_conditional_index_conditions.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/migrations/0006_auto_20230221_0802.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/migrations/0007_add_is_locked.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/migrations/0008_add_get_task_to_process_function.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/migrations/0009_add_recurring_task_run_first_run_at.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/migrations/0010_task_priority.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/migrations/0011_add_priority_to_get_tasks_to_process.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/migrations/0012_add_locked_at_and_timeout.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/migrations/0013_add_last_picked_at.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/migrations/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/migrations/sql/0008_get_recurring_tasks_to_process.sql +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/migrations/sql/0008_get_tasks_to_process.sql +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/migrations/sql/0011_get_tasks_to_process.sql +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/migrations/sql/0012_get_recurringtasks_to_process.sql +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/migrations/sql/0013_get_recurringtasks_to_process.sql +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/migrations/sql/__init__.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/models.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/monitoring.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/processor.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/py.typed +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/routers.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/serializers.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/task_registry.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/task_run_method.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/tasks.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/threads.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/types.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/urls.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/utils.py +0 -0
- {flagsmith_common-3.3.0 → flagsmith_common-3.5.0}/src/task_processor/views.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flagsmith-common
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.5.0
|
|
4
4
|
Summary: Flagsmith's common library
|
|
5
5
|
Author: Matthew Elwell, Gagan Trivedi, Kim Gustyr, Zach Aysan, Francesco Lo Franco, Rodrigo López Dato, Evandro Myller, Wadii Zaim
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -20,7 +20,10 @@ Requires-Dist: gunicorn>=19.1 ; extra == 'common-core'
|
|
|
20
20
|
Requires-Dist: prometheus-client>=0.0.16 ; extra == 'common-core'
|
|
21
21
|
Requires-Dist: psycopg2-binary>=2.9,<3 ; extra == 'common-core'
|
|
22
22
|
Requires-Dist: requests ; extra == 'common-core'
|
|
23
|
+
Requires-Dist: sentry-sdk>=2.0.0,<3.0.0 ; extra == 'common-core'
|
|
23
24
|
Requires-Dist: simplejson>=3,<4 ; extra == 'common-core'
|
|
25
|
+
Requires-Dist: structlog>=24.4,<26 ; extra == 'common-core'
|
|
26
|
+
Requires-Dist: typing-extensions ; extra == 'common-core'
|
|
24
27
|
Requires-Dist: simplejson ; extra == 'flagsmith-schemas'
|
|
25
28
|
Requires-Dist: typing-extensions ; extra == 'flagsmith-schemas'
|
|
26
29
|
Requires-Dist: flagsmith-flag-engine>6 ; extra == 'flagsmith-schemas'
|
|
@@ -81,6 +84,26 @@ This enables the `route` label for Prometheus HTTP metrics.
|
|
|
81
84
|
|
|
82
85
|
5. To enable the `/metrics` endpoint, set the `PROMETHEUS_ENABLED` setting to `True`.
|
|
83
86
|
|
|
87
|
+
### Pre-commit hooks
|
|
88
|
+
|
|
89
|
+
This repo provides a [`flagsmith-lint-tests`](.pre-commit-hooks.yaml) hook that enforces test conventions:
|
|
90
|
+
|
|
91
|
+
- **FT001**: No module-level `Test*` classes — use function-based tests
|
|
92
|
+
- **FT002**: No `import unittest` / `from unittest import TestCase` — use pytest (`unittest.mock` is fine)
|
|
93
|
+
- **FT003**: Test names must follow `test_{subject}__{condition}__{expected}`
|
|
94
|
+
- **FT004**: Test bodies must contain `# Given`, `# When`, and `# Then` comments
|
|
95
|
+
|
|
96
|
+
To use in your repo, add to `.pre-commit-config.yaml`:
|
|
97
|
+
|
|
98
|
+
```yaml
|
|
99
|
+
- repo: https://github.com/Flagsmith/flagsmith-common
|
|
100
|
+
rev: main
|
|
101
|
+
hooks:
|
|
102
|
+
- id: flagsmith-lint-tests
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Use `# noqa: FT003` (or any code) inline to suppress individual violations.
|
|
106
|
+
|
|
84
107
|
### Test tools
|
|
85
108
|
|
|
86
109
|
#### Fixtures
|
|
@@ -35,6 +35,26 @@ This enables the `route` label for Prometheus HTTP metrics.
|
|
|
35
35
|
|
|
36
36
|
5. To enable the `/metrics` endpoint, set the `PROMETHEUS_ENABLED` setting to `True`.
|
|
37
37
|
|
|
38
|
+
### Pre-commit hooks
|
|
39
|
+
|
|
40
|
+
This repo provides a [`flagsmith-lint-tests`](.pre-commit-hooks.yaml) hook that enforces test conventions:
|
|
41
|
+
|
|
42
|
+
- **FT001**: No module-level `Test*` classes — use function-based tests
|
|
43
|
+
- **FT002**: No `import unittest` / `from unittest import TestCase` — use pytest (`unittest.mock` is fine)
|
|
44
|
+
- **FT003**: Test names must follow `test_{subject}__{condition}__{expected}`
|
|
45
|
+
- **FT004**: Test bodies must contain `# Given`, `# When`, and `# Then` comments
|
|
46
|
+
|
|
47
|
+
To use in your repo, add to `.pre-commit-config.yaml`:
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
- repo: https://github.com/Flagsmith/flagsmith-common
|
|
51
|
+
rev: main
|
|
52
|
+
hooks:
|
|
53
|
+
- id: flagsmith-lint-tests
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Use `# noqa: FT003` (or any code) inline to suppress individual violations.
|
|
57
|
+
|
|
38
58
|
### Test tools
|
|
39
59
|
|
|
40
60
|
#### Fixtures
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "flagsmith-common"
|
|
3
|
-
version = "3.
|
|
3
|
+
version = "3.5.0"
|
|
4
4
|
description = "Flagsmith's common library"
|
|
5
5
|
requires-python = ">=3.11,<4.0"
|
|
6
6
|
dependencies = []
|
|
@@ -19,7 +19,10 @@ optional-dependencies = { test-tools = [
|
|
|
19
19
|
"prometheus-client (>=0.0.16)",
|
|
20
20
|
"psycopg2-binary (>=2.9,<3)",
|
|
21
21
|
"requests",
|
|
22
|
+
"sentry-sdk (>=2.0.0,<3.0.0)",
|
|
22
23
|
"simplejson (>=3,<4)",
|
|
24
|
+
"structlog (>=24.4,<26)",
|
|
25
|
+
"typing_extensions",
|
|
23
26
|
], task-processor = [
|
|
24
27
|
"backoff (>=2.2.1,<3.0.0)",
|
|
25
28
|
"django (>4,<6)",
|
|
@@ -60,12 +63,14 @@ Repository = "https://github.com/flagsmith/flagsmith-common"
|
|
|
60
63
|
|
|
61
64
|
[project.scripts]
|
|
62
65
|
flagsmith = "common.core.main:main"
|
|
66
|
+
flagsmith-lint-tests = "common.lint_tests:main"
|
|
63
67
|
|
|
64
68
|
[project.entry-points.pytest11]
|
|
65
69
|
flagsmith-test-tools = "common.test_tools.plugin"
|
|
66
70
|
|
|
67
71
|
[dependency-groups]
|
|
68
72
|
dev = [
|
|
73
|
+
"diff-cover>=10.2.0",
|
|
69
74
|
"dj-database-url (>=2.3.0, <3.0.0)",
|
|
70
75
|
"django-stubs (>=5.1.3, <6.0.0)",
|
|
71
76
|
"djangorestframework-stubs (>=3.15.3, <4.0.0)",
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import logging.config
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import structlog
|
|
10
|
+
import structlog.contextvars
|
|
11
|
+
import structlog.dev
|
|
12
|
+
import structlog.processors
|
|
13
|
+
import structlog.stdlib
|
|
14
|
+
from structlog.typing import EventDict, Processor, WrappedLogger
|
|
15
|
+
from typing_extensions import TypedDict
|
|
16
|
+
|
|
17
|
+
from common.core.constants import LOGGING_DEFAULT_ROOT_LOG_LEVEL
|
|
18
|
+
from common.core.sentry import sentry_processor
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class JsonRecord(TypedDict, extra_items=Any, total=False): # type: ignore[call-arg] # TODO https://github.com/python/mypy/issues/18176
|
|
24
|
+
levelname: str
|
|
25
|
+
message: str
|
|
26
|
+
timestamp: str
|
|
27
|
+
logger_name: str
|
|
28
|
+
pid: int | None
|
|
29
|
+
thread_name: str | None
|
|
30
|
+
exc_info: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def setup_logging(
|
|
34
|
+
log_level: str = "INFO",
|
|
35
|
+
log_format: str = "generic",
|
|
36
|
+
logging_configuration_file: str | None = None,
|
|
37
|
+
application_loggers: list[str] | None = None,
|
|
38
|
+
extra_foreign_processors: list[Processor] | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Set up logging for the application.
|
|
42
|
+
|
|
43
|
+
This should be called early, before Django settings are loaded, to ensure
|
|
44
|
+
that all log output is properly formatted from the start.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
log_level: The log level for application code (e.g. "DEBUG", "INFO").
|
|
48
|
+
log_format: Either "generic" or "json".
|
|
49
|
+
logging_configuration_file: Path to a JSON logging config file.
|
|
50
|
+
If provided, this takes precedence over other format options.
|
|
51
|
+
application_loggers: Top-level logger names for application packages.
|
|
52
|
+
These loggers are set to ``log_level`` while the root logger uses
|
|
53
|
+
``max(log_level, WARNING)`` to suppress noise from third-party
|
|
54
|
+
libraries. If ``log_level`` is DEBUG, everything logs at DEBUG.
|
|
55
|
+
extra_foreign_processors: Additional structlog processors to run in
|
|
56
|
+
the ``foreign_pre_chain`` for stdlib log records (e.g. the
|
|
57
|
+
Gunicorn access log field extractor).
|
|
58
|
+
"""
|
|
59
|
+
if logging_configuration_file:
|
|
60
|
+
with open(logging_configuration_file) as f:
|
|
61
|
+
config = json.load(f)
|
|
62
|
+
logging.config.dictConfig(config)
|
|
63
|
+
else:
|
|
64
|
+
log_level_int = logging.getLevelNamesMapping()[log_level.upper()]
|
|
65
|
+
root_level_int = logging.getLevelNamesMapping()[LOGGING_DEFAULT_ROOT_LOG_LEVEL]
|
|
66
|
+
# Suppress third-party noise at WARNING, but if the user requests
|
|
67
|
+
# DEBUG, honour that for the entire process.
|
|
68
|
+
effective_root_level = (
|
|
69
|
+
log_level_int if log_level_int < logging.INFO else root_level_int
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
dict_config: dict[str, Any] = {
|
|
73
|
+
"version": 1,
|
|
74
|
+
"disable_existing_loggers": False,
|
|
75
|
+
"handlers": {
|
|
76
|
+
"console": {
|
|
77
|
+
"class": "logging.StreamHandler",
|
|
78
|
+
"stream": "ext://sys.stdout",
|
|
79
|
+
"level": log_level,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
"root": {
|
|
83
|
+
"level": effective_root_level,
|
|
84
|
+
"handlers": ["console"],
|
|
85
|
+
},
|
|
86
|
+
"loggers": {
|
|
87
|
+
name: {"level": log_level, "handlers": [], "propagate": True}
|
|
88
|
+
for name in application_loggers or []
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
logging.config.dictConfig(dict_config)
|
|
92
|
+
|
|
93
|
+
setup_structlog(
|
|
94
|
+
log_format=log_format, extra_foreign_processors=extra_foreign_processors
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def map_event_to_json_record(
|
|
99
|
+
logger: WrappedLogger,
|
|
100
|
+
method_name: str,
|
|
101
|
+
event_dict: EventDict,
|
|
102
|
+
) -> EventDict:
|
|
103
|
+
"""Map structlog fields to match :class:`JsonRecord` output schema."""
|
|
104
|
+
# Remove foreign record args injected by pass_foreign_args so they
|
|
105
|
+
# don't leak into the rendered JSON output.
|
|
106
|
+
event_dict.pop("positional_args", None)
|
|
107
|
+
record: JsonRecord = {
|
|
108
|
+
"message": event_dict.pop("event", ""),
|
|
109
|
+
"levelname": event_dict.pop("level", "").upper(),
|
|
110
|
+
"logger_name": event_dict.pop("logger", ""),
|
|
111
|
+
"pid": os.getpid(),
|
|
112
|
+
"thread_name": threading.current_thread().name,
|
|
113
|
+
}
|
|
114
|
+
if "exception" in event_dict:
|
|
115
|
+
record["exc_info"] = event_dict.pop("exception")
|
|
116
|
+
# Merge remaining structlog keys (e.g. extra_key from bind()) with the
|
|
117
|
+
# canonical record so they appear in the JSON output.
|
|
118
|
+
event_dict.update(record)
|
|
119
|
+
return event_dict
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def setup_structlog(
|
|
123
|
+
log_format: str,
|
|
124
|
+
extra_foreign_processors: list[Processor] | None = None,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Configure structlog to route through stdlib logging."""
|
|
127
|
+
|
|
128
|
+
if log_format == "json":
|
|
129
|
+
renderer_processors: list[Processor] = [
|
|
130
|
+
map_event_to_json_record,
|
|
131
|
+
structlog.processors.JSONRenderer(),
|
|
132
|
+
]
|
|
133
|
+
else:
|
|
134
|
+
colors = sys.stdout.isatty() and structlog.dev._has_colors
|
|
135
|
+
renderer_processors = [
|
|
136
|
+
structlog.dev.ConsoleRenderer(colors=colors),
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
foreign_pre_chain: list[Processor] = [
|
|
140
|
+
structlog.contextvars.merge_contextvars,
|
|
141
|
+
structlog.stdlib.add_logger_name,
|
|
142
|
+
structlog.stdlib.add_log_level,
|
|
143
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
144
|
+
structlog.processors.format_exc_info,
|
|
145
|
+
structlog.stdlib.ExtraAdder(),
|
|
146
|
+
*(extra_foreign_processors or []),
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
formatter = structlog.stdlib.ProcessorFormatter(
|
|
150
|
+
processors=[
|
|
151
|
+
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
|
152
|
+
*renderer_processors,
|
|
153
|
+
],
|
|
154
|
+
foreign_pre_chain=foreign_pre_chain,
|
|
155
|
+
pass_foreign_args=True,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Replace the formatter on existing root handlers with ProcessorFormatter.
|
|
159
|
+
root = logging.getLogger()
|
|
160
|
+
for handler in root.handlers:
|
|
161
|
+
handler.setFormatter(formatter)
|
|
162
|
+
|
|
163
|
+
structlog.configure(
|
|
164
|
+
processors=[
|
|
165
|
+
structlog.contextvars.merge_contextvars,
|
|
166
|
+
structlog.stdlib.filter_by_level,
|
|
167
|
+
structlog.stdlib.add_logger_name,
|
|
168
|
+
structlog.stdlib.add_log_level,
|
|
169
|
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
170
|
+
structlog.processors.StackInfoRenderer(),
|
|
171
|
+
structlog.processors.UnicodeDecoder(),
|
|
172
|
+
structlog.processors.format_exc_info,
|
|
173
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
174
|
+
sentry_processor,
|
|
175
|
+
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
|
176
|
+
],
|
|
177
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
178
|
+
context_class=dict,
|
|
179
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
180
|
+
cache_logger_on_first_use=True,
|
|
181
|
+
)
|
|
@@ -8,8 +8,13 @@ from tempfile import mkdtemp
|
|
|
8
8
|
from django.core.management import (
|
|
9
9
|
execute_from_command_line as django_execute_from_command_line,
|
|
10
10
|
)
|
|
11
|
+
from environs import Env
|
|
11
12
|
|
|
12
13
|
from common.core.cli import healthcheck
|
|
14
|
+
from common.core.logging import setup_logging
|
|
15
|
+
from common.gunicorn.processors import make_gunicorn_access_processor
|
|
16
|
+
|
|
17
|
+
env = Env()
|
|
13
18
|
|
|
14
19
|
logger = logging.getLogger(__name__)
|
|
15
20
|
|
|
@@ -32,7 +37,18 @@ def ensure_cli_env() -> typing.Generator[None, None, None]:
|
|
|
32
37
|
"""
|
|
33
38
|
ctx = contextlib.ExitStack()
|
|
34
39
|
|
|
35
|
-
#
|
|
40
|
+
# Set up logging early, before Django settings are loaded.
|
|
41
|
+
setup_logging(
|
|
42
|
+
log_level=env.str("LOG_LEVEL", "INFO"),
|
|
43
|
+
log_format=env.str("LOG_FORMAT", "generic"),
|
|
44
|
+
logging_configuration_file=env.str("LOGGING_CONFIGURATION_FILE", None),
|
|
45
|
+
application_loggers=env.list("APPLICATION_LOGGERS", []) or None,
|
|
46
|
+
extra_foreign_processors=[
|
|
47
|
+
make_gunicorn_access_processor(
|
|
48
|
+
env.list("ACCESS_LOG_EXTRA_ITEMS", []) or None,
|
|
49
|
+
),
|
|
50
|
+
],
|
|
51
|
+
)
|
|
36
52
|
|
|
37
53
|
# Prometheus multiproc support
|
|
38
54
|
if not os.environ.get("PROMETHEUS_MULTIPROC_DIR"):
|
|
@@ -67,12 +83,13 @@ def execute_from_command_line(argv: list[str]) -> None:
|
|
|
67
83
|
"checktaskprocessorthreadhealth": healthcheck.main,
|
|
68
84
|
}[subcommand]
|
|
69
85
|
except (IndexError, KeyError):
|
|
70
|
-
|
|
86
|
+
logger.info("Invoking Django")
|
|
71
87
|
else:
|
|
72
|
-
subcommand_main(
|
|
88
|
+
return subcommand_main(
|
|
73
89
|
argv[2:],
|
|
74
90
|
prog=f"{os.path.basename(argv[0])} {subcommand}",
|
|
75
91
|
)
|
|
92
|
+
django_execute_from_command_line(argv)
|
|
76
93
|
|
|
77
94
|
|
|
78
95
|
def main(argv: list[str] = sys.argv) -> None:
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import sentry_sdk
|
|
2
|
+
from structlog.typing import EventDict, WrappedLogger
|
|
3
|
+
|
|
4
|
+
_SKIP_CONTEXT_FIELDS = frozenset({"event", "level", "timestamp", "_record"})
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def sentry_processor(
|
|
8
|
+
logger: WrappedLogger,
|
|
9
|
+
method_name: str,
|
|
10
|
+
event_dict: EventDict,
|
|
11
|
+
) -> EventDict:
|
|
12
|
+
"""
|
|
13
|
+
Structlog processor that enriches Sentry with structured context and tags.
|
|
14
|
+
|
|
15
|
+
Since structlog routes through stdlib, Sentry's LoggingIntegration
|
|
16
|
+
automatically captures ERROR+ logs as Sentry events. This processor
|
|
17
|
+
adds structured context on top of that.
|
|
18
|
+
"""
|
|
19
|
+
context = {k: v for k, v in event_dict.items() if k not in _SKIP_CONTEXT_FIELDS}
|
|
20
|
+
sentry_sdk.set_context("structlog", context)
|
|
21
|
+
|
|
22
|
+
return event_dict
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from gunicorn.config import Config # type: ignore[import-untyped]
|
|
7
|
+
from gunicorn.http.message import Request # type: ignore[import-untyped]
|
|
8
|
+
from gunicorn.http.wsgi import Response # type: ignore[import-untyped]
|
|
9
|
+
from gunicorn.instrument.statsd import ( # type: ignore[import-untyped]
|
|
10
|
+
Statsd as StatsdGunicornLogger,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from common.gunicorn import metrics
|
|
14
|
+
from common.gunicorn.utils import get_extra
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PrometheusGunicornLogger(StatsdGunicornLogger): # type: ignore[misc]
|
|
18
|
+
"""Gunicorn logger that records Prometheus metrics on each access log entry."""
|
|
19
|
+
|
|
20
|
+
def access(
|
|
21
|
+
self,
|
|
22
|
+
resp: Response,
|
|
23
|
+
req: Request,
|
|
24
|
+
environ: dict[str, Any],
|
|
25
|
+
request_time: timedelta,
|
|
26
|
+
) -> None:
|
|
27
|
+
super().access(resp, req, environ, request_time)
|
|
28
|
+
duration_seconds = (
|
|
29
|
+
request_time.seconds + float(request_time.microseconds) / 10**6
|
|
30
|
+
)
|
|
31
|
+
labels = {
|
|
32
|
+
# To avoid cardinality explosion, we use a resolved Django route
|
|
33
|
+
# instead of raw path.
|
|
34
|
+
# The Django route is set by `RouteLoggerMiddleware`.
|
|
35
|
+
"route": get_extra(environ=environ, key="route") or "",
|
|
36
|
+
"method": environ.get("REQUEST_METHOD") or "",
|
|
37
|
+
"response_status": resp.status_code,
|
|
38
|
+
}
|
|
39
|
+
metrics.flagsmith_http_server_request_duration_seconds.labels(**labels).observe(
|
|
40
|
+
duration_seconds
|
|
41
|
+
)
|
|
42
|
+
metrics.flagsmith_http_server_requests_total.labels(**labels).inc()
|
|
43
|
+
metrics.flagsmith_http_server_response_size_bytes.labels(**labels).observe(
|
|
44
|
+
getattr(resp, "sent", 0),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class GunicornJsonCapableLogger(PrometheusGunicornLogger):
|
|
49
|
+
"""Gunicorn logger that integrates with the application logging setup."""
|
|
50
|
+
|
|
51
|
+
def setup(self, cfg: Config) -> None:
|
|
52
|
+
super().setup(cfg)
|
|
53
|
+
|
|
54
|
+
# Error log always propagates to root.
|
|
55
|
+
for handler in self.error_log.handlers[:]:
|
|
56
|
+
self.error_log.removeHandler(handler)
|
|
57
|
+
self.error_log.propagate = True
|
|
58
|
+
|
|
59
|
+
# In JSON mode, replace the access log formatter with the root's
|
|
60
|
+
# ProcessorFormatter. In generic mode, keep Gunicorn's CLF formatter.
|
|
61
|
+
# The handler itself is preserved so ACCESS_LOG_LOCATION is respected.
|
|
62
|
+
root_handlers = logging.getLogger().handlers
|
|
63
|
+
if os.environ.get("LOG_FORMAT") == "json" and root_handlers:
|
|
64
|
+
root_formatter = root_handlers[0].formatter
|
|
65
|
+
for handler in self.access_log.handlers:
|
|
66
|
+
handler.setFormatter(root_formatter)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import prometheus_client
|
|
2
|
-
from django.conf import settings
|
|
3
2
|
|
|
4
3
|
from common.gunicorn.constants import HTTP_SERVER_RESPONSE_SIZE_DEFAULT_BUCKETS
|
|
5
4
|
from common.prometheus import Histogram
|
|
@@ -18,9 +17,5 @@ flagsmith_http_server_response_size_bytes = Histogram(
|
|
|
18
17
|
"flagsmith_http_server_response_size_bytes",
|
|
19
18
|
"HTTP response size in bytes.",
|
|
20
19
|
["route", "method", "response_status"],
|
|
21
|
-
buckets=
|
|
22
|
-
settings,
|
|
23
|
-
"PROMETHEUS_HTTP_SERVER_RESPONSE_SIZE_HISTOGRAM_BUCKETS",
|
|
24
|
-
HTTP_SERVER_RESPONSE_SIZE_DEFAULT_BUCKETS,
|
|
25
|
-
),
|
|
20
|
+
buckets=HTTP_SERVER_RESPONSE_SIZE_DEFAULT_BUCKETS,
|
|
26
21
|
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from structlog.typing import EventDict, Processor, WrappedLogger
|
|
4
|
+
|
|
5
|
+
from common.gunicorn.constants import (
|
|
6
|
+
WSGI_EXTRA_SUFFIX_TO_CATEGORY,
|
|
7
|
+
wsgi_extra_key_regex,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def make_gunicorn_access_processor(
|
|
12
|
+
access_log_extra_items: list[str] | None = None,
|
|
13
|
+
) -> Processor:
|
|
14
|
+
"""Create a processor that extracts structured fields from Gunicorn access logs.
|
|
15
|
+
|
|
16
|
+
Gunicorn populates ``record.args`` with a dict of request/response data
|
|
17
|
+
(keyed by format variables like ``h``, ``m``, ``s``, ``U``, etc.). This
|
|
18
|
+
processor detects those records and promotes the data into the event dict
|
|
19
|
+
so it flows through the normal rendering pipeline.
|
|
20
|
+
|
|
21
|
+
Pass the returned processor to :func:`~common.core.logging.setup_logging`
|
|
22
|
+
via ``extra_foreign_processors``.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def processor(
|
|
26
|
+
logger: WrappedLogger,
|
|
27
|
+
method_name: str,
|
|
28
|
+
event_dict: EventDict,
|
|
29
|
+
) -> EventDict:
|
|
30
|
+
record = event_dict.get("_record")
|
|
31
|
+
if record is None or record.name != "gunicorn.access":
|
|
32
|
+
return event_dict
|
|
33
|
+
# Gunicorn passes request data as a dict in record.args.
|
|
34
|
+
# By the time foreign_pre_chain runs, ProcessorFormatter has
|
|
35
|
+
# cleared args on its copy; positional_args has the originals.
|
|
36
|
+
args = event_dict.get("positional_args", record.args)
|
|
37
|
+
if not isinstance(args, dict):
|
|
38
|
+
return event_dict
|
|
39
|
+
|
|
40
|
+
url = args.get("U", "")
|
|
41
|
+
if q := args.get("q"):
|
|
42
|
+
url += f"?{q}"
|
|
43
|
+
|
|
44
|
+
if t := args.get("t"):
|
|
45
|
+
event_dict["time"] = datetime.strptime(
|
|
46
|
+
t, "[%d/%b/%Y:%H:%M:%S %z]"
|
|
47
|
+
).isoformat()
|
|
48
|
+
event_dict["path"] = url
|
|
49
|
+
event_dict["remote_ip"] = args.get("h", "")
|
|
50
|
+
event_dict["method"] = args.get("m", "")
|
|
51
|
+
event_dict["status"] = str(args.get("s", ""))
|
|
52
|
+
event_dict["user_agent"] = args.get("a", "")
|
|
53
|
+
event_dict["duration_in_ms"] = args.get("M", 0)
|
|
54
|
+
event_dict["response_size_in_bytes"] = args.get("B") or 0
|
|
55
|
+
|
|
56
|
+
if access_log_extra_items:
|
|
57
|
+
for extra_key in access_log_extra_items:
|
|
58
|
+
extra_key_lower = extra_key.lower()
|
|
59
|
+
if (
|
|
60
|
+
(extra_value := args.get(extra_key_lower))
|
|
61
|
+
and (re_match := wsgi_extra_key_regex.match(extra_key_lower))
|
|
62
|
+
and (
|
|
63
|
+
category := WSGI_EXTRA_SUFFIX_TO_CATEGORY.get(
|
|
64
|
+
re_match.group("suffix")
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
):
|
|
68
|
+
event_dict.setdefault(category, {})[re_match.group("key")] = (
|
|
69
|
+
extra_value
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return event_dict
|
|
73
|
+
|
|
74
|
+
return processor
|