Skip to content

Commit efc98ef

Browse files
khvn26Rodrigo López Dato
andauthored
feat: Prometheus support, core entrypoint, packaging improvements (#17)
- Add Prometheus support - Cover Gunicorn with request metrics - Add support for custom application metrics - Add `flagsmith-test-tools` pytest plugin and `assert_metric` fixture - Add `flagsmith start api` package entrypoint - Improve packaging --------- Co-authored-by: Rodrigo López Dato <[email protected]>
1 parent bd88fbd commit efc98ef

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1186
-104
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
.venv/
22
.env
33
.idea/
4-
*.pyc
4+
*.pyc
5+
.coverage
6+
common.sqlite3

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
BSD 3-Clause License
22

3-
Copyright (c) 2024, Flagsmith
3+
Copyright (c) 2025, Flagsmith
44

55
Redistribution and use in source and binary forms, with or without
66
modification, are permitted provided that the following conditions are met:

README.md

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# flagsmith-common
2-
A repository for including code that is required in multiple flagsmith repositories
2+
Flagsmith's common library
33

44
### Development Setup
55

66
This project uses [Poetry](https://python-poetry.org/) for dependency management and includes a Makefile to simplify common development tasks.
77

88
#### Prerequisites
99

10-
- Python >= 3.8
10+
- Python >= 3.11
1111
- Make
1212

1313
#### Installation
@@ -47,3 +47,49 @@ make install-packages opts="--with dev"
4747
# Install with specific extras
4848
make install-packages opts="--extras 'feature1 feature2'"
4949
```
50+
51+
### Usage
52+
53+
#### Installation
54+
55+
1. Make sure `"common.core"` is in the `INSTALLED_APPS` of your settings module.
56+
This enables the `manage.py flagsmith` commands.
57+
58+
2. Add `"common.gunicorn.middleware.RouteLoggerMiddleware"` to `MIDDLEWARE` in your settings module.
59+
This enables the `route` label for Prometheus HTTP metrics.
60+
61+
3. To enable the `/metrics` endpoint, set the `PROMETHEUS_ENABLED` setting to `True`.
62+
63+
#### Metrics
64+
65+
Flagsmith uses Prometheus to track performance metrics.
66+
67+
The following default metrics are exposed:
68+
69+
- `flagsmith_build_info`: Has the labels `version` and `ci_commit_sha`.
70+
- `http_server_request_duration_seconds`: Histogram labeled with `method`, `path`, and `response_status`.
71+
- `http_server_requests_total`: Counter labeled with `method`, `path`, and `response_status`.
72+
73+
##### Guidelines
74+
75+
Try to come up with meaningful metrics to cover your feature with when developing it. Refer to [Prometheus best practices][1] when naming your metric and labels.
76+
77+
Define your metrics in a `metrics.py` module of your Django application — see [example][2]. Contrary to Prometheus Python client examples and documentation, please name a metric variable exactly as your metric name.
78+
79+
It's generally a good idea to allow users to define histogram buckets of their own. Flagsmith accepts a `PROMETHEUS_HISTOGRAM_BUCKETS` setting so users can customise their buckets. To honour the setting, use the `common.prometheus.Histogram` class when defining your histograms. When using `prometheus_client.Histogram` directly, please expose a dedicated setting like so:
80+
81+
```python
82+
import prometheus_client
83+
from django.conf import settings
84+
85+
distance_from_earth_au = prometheus.Histogram(
86+
"distance_from_earth_au",
87+
"Distance from Earth in astronomical units",
88+
buckets=settings.DISTANCE_FROM_EARTH_AU_HISTOGRAM_BUCKETS,
89+
)
90+
```
91+
92+
[1]: https://prometheus.io/docs/practices/naming/
93+
[2]: https://github.com/Flagsmith/flagsmith-common/blob/main/src/common/gunicorn/metrics.py
94+
[3]: https://docs.gunicorn.org/en/stable/design.html#server-model
95+
[4]: https://prometheus.github.io/client_python/multiprocess

poetry.lock

Lines changed: 300 additions & 62 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,70 @@
1-
[tool.poetry]
2-
name = "flagsmith_common"
1+
[project]
2+
name = "flagsmith-common"
33
version = "1.5.0"
4-
description = "A repository for including code that is required in multiple flagsmith repositories"
5-
authors = ["Matthew Elwell <[email protected]>"]
4+
description = "Flagsmith's common library"
5+
requires-python = ">=3.11,<4.0"
6+
dependencies = [
7+
"django (<5)",
8+
"django-health-check",
9+
"djangorestframework-recursive",
10+
"djangorestframework",
11+
"drf-writable-nested",
12+
"flagsmith-flag-engine",
13+
"gunicorn (>=19.1)",
14+
"prometheus-client (>=0.0.16)",
15+
"environs (<15)",
16+
]
17+
authors = [
18+
{ name = "Matthew Elwell" },
19+
{ name = "Gagan Trivedi" },
20+
{ name = "Kim Gustyr" },
21+
{ name = "Zach Aysan" },
22+
{ name = "Francesco Lo Franco" },
23+
{ name = "Rodrigo López Dato" },
24+
]
25+
maintainers = [{ name = "Flagsmith Team", email = "[email protected]" }]
26+
license = "BSD-3-Clause"
27+
license-files = ["LICENSE"]
628
readme = "README.md"
7-
packages = [{ include = "common", from = "src" }]
29+
dynamic = ["classifiers"]
30+
31+
[project.urls]
32+
Download = "https://github.com/flagsmith/flagsmith-common/releases"
33+
Homepage = "https://flagsmith.com"
34+
Issues = "https://github.com/flagsmith/flagsmith-common/issues"
35+
Repository = "https://github.com/flagsmith/flagsmith-common"
836

9-
[tool.poetry.dependencies]
10-
python = "^3.10"
11-
django = "<5.0.0"
12-
djangorestframework = "*"
13-
drf-writable-nested = "*"
14-
flagsmith-flag-engine = "*"
15-
djangorestframework-recursive = "*"
16-
django-health-check = "^3.18.3"
37+
[project.scripts]
38+
flagsmith = "common.core.main:main"
39+
40+
[project.entry-points.pytest11]
41+
flagsmith-test-tools = "common.test_tools.plugin"
42+
43+
[tool.poetry]
44+
requires-poetry = ">=2.0"
45+
classifiers = [
46+
"Framework :: Django",
47+
"Framework :: Pytest",
48+
"Intended Audience :: Developers",
49+
"License :: OSI Approved :: BSD License",
50+
]
51+
packages = [{ include = "common", from = "src" }]
1752

1853
[tool.poetry.group.dev.dependencies]
54+
django-stubs = "^5.1.3"
55+
djangorestframework-stubs = "^3.15.3"
56+
mypy = "^1.15.0"
1957
pre-commit = "*"
20-
ruff = "*"
21-
pytest = "^8.3.4"
2258
pyfakefs = "^5.7.4"
59+
pytest = "^8.3.4"
60+
pytest-asyncio = "^0.25.3"
61+
pytest-cov = "^6.0.0"
2362
pytest-django = "^4.10.0"
24-
mypy = "^1.15.0"
25-
django-stubs = "^5.1.3"
26-
djangorestframework-stubs = "^3.15.3"
63+
pytest-freezegun = "^0.4.2"
64+
pytest-mock = "^3.14.0"
65+
requests = "^2.32.3"
66+
ruff = "*"
67+
setuptools = "^77.0.3"
2768

2869
[build-system]
2970
requires = ["poetry-core"]

settings/dev.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
1+
import prometheus_client
2+
13
# Settings expected by `mypy_django_plugin`
2-
SENDGRID_API_KEY: str
34
AWS_SES_REGION_ENDPOINT: str
45
SEGMENT_RULES_CONDITIONS_LIMIT: int
6+
SENDGRID_API_KEY: str
7+
PROMETHEUS_ENABLED = True
58

69
# Settings required for tests
7-
INSTALLED_APPS = [
8-
"django.contrib.auth",
9-
"django.contrib.contenttypes",
10-
]
1110
DATABASES = {
1211
"default": {
1312
"ENGINE": "django.db.backends.sqlite3",
14-
"NAME": "common",
13+
"NAME": "common.sqlite3",
1514
}
1615
}
16+
INSTALLED_APPS = [
17+
"django.contrib.auth",
18+
"django.contrib.contenttypes",
19+
"common.core",
20+
]
21+
MIDDLEWARE = [
22+
"common.gunicorn.middleware.RouteLoggerMiddleware",
23+
]
24+
LOG_FORMAT = "json"
25+
PROMETHEUS_HISTOGRAM_BUCKETS = prometheus_client.Histogram.DEFAULT_BUCKETS
26+
ROOT_URLCONF = "common.core.urls"
27+
TIME_ZONE = "UTC"
28+
USE_TZ = True

src/common/app/views.py

Lines changed: 0 additions & 12 deletions
This file was deleted.
File renamed without changes.

src/common/core/app.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class CoreConfig(AppConfig):
5+
name = "common.core"
6+
label = "common_core"

src/common/core/logging.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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))

0 commit comments

Comments
 (0)