c2cwsgiutils 6.2.0.dev36__py3-none-any.whl → 6.2.0.dev41__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.
- c2cwsgiutils/loader.py +20 -2
- c2cwsgiutils/prometheus.py +56 -0
- c2cwsgiutils/setup_process.py +2 -0
- {c2cwsgiutils-6.2.0.dev36.dist-info → c2cwsgiutils-6.2.0.dev41.dist-info}/METADATA +73 -12
- {c2cwsgiutils-6.2.0.dev36.dist-info → c2cwsgiutils-6.2.0.dev41.dist-info}/RECORD +8 -8
- {c2cwsgiutils-6.2.0.dev36.dist-info → c2cwsgiutils-6.2.0.dev41.dist-info}/LICENSE +0 -0
- {c2cwsgiutils-6.2.0.dev36.dist-info → c2cwsgiutils-6.2.0.dev41.dist-info}/WHEEL +0 -0
- {c2cwsgiutils-6.2.0.dev36.dist-info → c2cwsgiutils-6.2.0.dev41.dist-info}/entry_points.txt +0 -0
c2cwsgiutils/loader.py
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
import logging
|
1
|
+
import logging.config
|
2
2
|
from typing import Optional, cast
|
3
3
|
|
4
4
|
from plaster_pastedeploy import Loader as BaseLoader
|
5
5
|
|
6
|
-
from c2cwsgiutils import get_config_defaults
|
6
|
+
from c2cwsgiutils import get_config_defaults, get_logconfig_dict
|
7
7
|
|
8
8
|
_LOG = logging.getLogger(__name__)
|
9
9
|
|
@@ -19,3 +19,21 @@ class Loader(BaseLoader): # type: ignore
|
|
19
19
|
def __repr__(self) -> str:
|
20
20
|
"""Get the object representation."""
|
21
21
|
return f'c2cwsgiutils.loader.Loader(uri="{self.uri}")'
|
22
|
+
|
23
|
+
def setup_logging(self, defaults: Optional[dict[str, str]] = None) -> None:
|
24
|
+
"""
|
25
|
+
Set up logging via :func:`logging.config.fileConfig`.
|
26
|
+
|
27
|
+
Defaults are specified for the special ``__file__`` and ``here``
|
28
|
+
variables, similar to PasteDeploy config loading. Extra defaults can
|
29
|
+
optionally be specified as a dict in ``defaults``.
|
30
|
+
|
31
|
+
Arguments:
|
32
|
+
defaults: The defaults that will be used when passed to
|
33
|
+
:func:`logging.config.fileConfig`.
|
34
|
+
|
35
|
+
"""
|
36
|
+
if "loggers" in self.get_sections():
|
37
|
+
logging.config.dictConfig(get_logconfig_dict(self.uri.path))
|
38
|
+
else:
|
39
|
+
logging.basicConfig()
|
c2cwsgiutils/prometheus.py
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
import os
|
4
4
|
import re
|
5
|
+
import resource
|
5
6
|
from collections.abc import Generator, Iterable
|
6
7
|
from typing import Any, Optional, TypedDict, cast
|
7
8
|
|
@@ -15,6 +16,12 @@ import pyramid.config
|
|
15
16
|
from c2cwsgiutils import broadcast, redis_utils
|
16
17
|
from c2cwsgiutils.debug.utils import dump_memory_maps
|
17
18
|
|
19
|
+
PSUTILS = True
|
20
|
+
try:
|
21
|
+
import psutil
|
22
|
+
except ImportError:
|
23
|
+
PSUTILS = False
|
24
|
+
|
18
25
|
_NUMBER_RE = re.compile(r"^[0-9]+$")
|
19
26
|
MULTI_PROCESS_COLLECTOR_BROADCAST_CHANNELS = [
|
20
27
|
"c2cwsgiutils_prometheus_collector_gc",
|
@@ -22,12 +29,21 @@ MULTI_PROCESS_COLLECTOR_BROADCAST_CHANNELS = [
|
|
22
29
|
]
|
23
30
|
|
24
31
|
|
32
|
+
def start_single_process() -> None:
|
33
|
+
"""Start separate HTTP server to provide the Prometheus metrics."""
|
34
|
+
if os.environ.get("C2C_PROMETHEUS_PORT") is not None:
|
35
|
+
prometheus_client.REGISTRY.register(ResourceCollector())
|
36
|
+
prometheus_client.REGISTRY.register(MemoryInfoCollector())
|
37
|
+
prometheus_client.start_http_server(int(os.environ["C2C_PROMETHEUS_PORT"]))
|
38
|
+
|
39
|
+
|
25
40
|
def start(registry: Optional[prometheus_client.CollectorRegistry] = None) -> None:
|
26
41
|
"""Start separate HTTP server to provide the Prometheus metrics."""
|
27
42
|
if os.environ.get("C2C_PROMETHEUS_PORT") is not None:
|
28
43
|
broadcast.includeme()
|
29
44
|
|
30
45
|
registry = prometheus_client.CollectorRegistry() if registry is None else registry
|
46
|
+
registry.register(ResourceCollector())
|
31
47
|
registry.register(MemoryMapCollector())
|
32
48
|
registry.register(prometheus_client.PLATFORM_COLLECTOR)
|
33
49
|
registry.register(MultiProcessCustomCollector())
|
@@ -181,3 +197,43 @@ class MemoryMapCollector(prometheus_client.registry.Collector):
|
|
181
197
|
for e in dump_memory_maps(pid):
|
182
198
|
gauge.add_metric([pid, e["name"]], e[self.memory_type + "_kb"] * 1024)
|
183
199
|
yield gauge
|
200
|
+
|
201
|
+
|
202
|
+
class ResourceCollector(prometheus_client.registry.Collector):
|
203
|
+
"""Collect the resources used by Python."""
|
204
|
+
|
205
|
+
def collect(self) -> Generator[prometheus_client.core.GaugeMetricFamily, None, None]:
|
206
|
+
"""Get the gauge from smap file."""
|
207
|
+
gauge = prometheus_client.core.GaugeMetricFamily(
|
208
|
+
build_metric_name("python_resource"),
|
209
|
+
"Python resources",
|
210
|
+
labels=["name"],
|
211
|
+
)
|
212
|
+
r = resource.getrusage(resource.RUSAGE_SELF)
|
213
|
+
for field in dir(r):
|
214
|
+
if field.startswith("ru_"):
|
215
|
+
gauge.add_metric([field[3:]], getattr(r, field))
|
216
|
+
yield gauge
|
217
|
+
|
218
|
+
|
219
|
+
class MemoryInfoCollector(prometheus_client.registry.Collector):
|
220
|
+
"""Collect the resources used by Python."""
|
221
|
+
|
222
|
+
process = psutil.Process(os.getpid())
|
223
|
+
|
224
|
+
def collect(self) -> Generator[prometheus_client.core.GaugeMetricFamily, None, None]:
|
225
|
+
"""Get the gauge from smap file."""
|
226
|
+
gauge = prometheus_client.core.GaugeMetricFamily(
|
227
|
+
build_metric_name("python_memory_info"),
|
228
|
+
"Python memory info",
|
229
|
+
labels=["name"],
|
230
|
+
)
|
231
|
+
memory_info = self.process.memory_info()
|
232
|
+
gauge.add_metric(["rss"], memory_info.rss)
|
233
|
+
gauge.add_metric(["vms"], memory_info.vms)
|
234
|
+
gauge.add_metric(["shared"], memory_info.shared)
|
235
|
+
gauge.add_metric(["text"], memory_info.text)
|
236
|
+
gauge.add_metric(["lib"], memory_info.lib)
|
237
|
+
gauge.add_metric(["data"], memory_info.data)
|
238
|
+
gauge.add_metric(["dirty"], memory_info.dirty)
|
239
|
+
yield gauge
|
c2cwsgiutils/setup_process.py
CHANGED
@@ -5,6 +5,7 @@ Must be imported at the very beginning of the process's life, before any other m
|
|
5
5
|
"""
|
6
6
|
|
7
7
|
import argparse
|
8
|
+
import logging
|
8
9
|
import warnings
|
9
10
|
from typing import Any, Callable, Optional, TypedDict, cast
|
10
11
|
|
@@ -97,4 +98,5 @@ def bootstrap_application(
|
|
97
98
|
"""
|
98
99
|
loader = get_config_loader(config_uri)
|
99
100
|
loader.setup_logging(options)
|
101
|
+
logging.getLogger(__name__).info("Loading the application from %s", config_uri)
|
100
102
|
return cast(PyramidEnv, bootstrap(config_uri, options=options))
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: c2cwsgiutils
|
3
|
-
Version: 6.2.0.
|
3
|
+
Version: 6.2.0.dev41
|
4
4
|
Summary: Common utilities for Camptocamp WSGI applications
|
5
5
|
Home-page: https://github.com/camptocamp/c2cwsgiutils
|
6
6
|
License: BSD-2-Clause
|
@@ -33,23 +33,26 @@ Provides-Extra: sentry
|
|
33
33
|
Provides-Extra: standard
|
34
34
|
Provides-Extra: test-images
|
35
35
|
Provides-Extra: tests
|
36
|
+
Provides-Extra: waitress
|
36
37
|
Provides-Extra: webserver
|
37
|
-
Requires-Dist:
|
38
|
-
Requires-Dist: SQLAlchemy
|
38
|
+
Requires-Dist: Paste ; extra == "standard" or extra == "waitress" or extra == "all"
|
39
|
+
Requires-Dist: SQLAlchemy ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
|
40
|
+
Requires-Dist: SQLAlchemy-Utils ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
|
39
41
|
Requires-Dist: alembic ; extra == "standard" or extra == "alembic" or extra == "all"
|
40
42
|
Requires-Dist: boltons ; extra == "tests" or extra == "all"
|
41
43
|
Requires-Dist: cee_syslog_handler
|
42
|
-
Requires-Dist: cornice ; extra == "standard" or extra == "webserver" or extra == "all"
|
44
|
+
Requires-Dist: cornice ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
|
45
|
+
Requires-Dist: coverage ; extra == "debug" or extra == "all"
|
43
46
|
Requires-Dist: gunicorn ; extra == "standard" or extra == "webserver" or extra == "all"
|
44
47
|
Requires-Dist: lxml ; extra == "tests" or extra == "all"
|
45
48
|
Requires-Dist: objgraph ; extra == "debug" or extra == "all"
|
46
|
-
Requires-Dist: prometheus-client ; extra == "standard" or extra == "webserver" or extra == "all"
|
49
|
+
Requires-Dist: prometheus-client ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
|
47
50
|
Requires-Dist: psutil ; extra == "debug" or extra == "all"
|
48
|
-
Requires-Dist: psycopg2 ; extra == "standard" or extra == "webserver" or extra == "all"
|
51
|
+
Requires-Dist: psycopg2 ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
|
49
52
|
Requires-Dist: pyjwt ; extra == "standard" or extra == "oauth2" or extra == "all"
|
50
|
-
Requires-Dist: pyramid ; extra == "standard" or extra == "webserver" or extra == "all"
|
51
|
-
Requires-Dist: pyramid-tm ; extra == "standard" or extra == "webserver" or extra == "all"
|
52
|
-
Requires-Dist: pyramid_mako ; extra == "standard" or extra == "webserver" or extra == "all"
|
53
|
+
Requires-Dist: pyramid ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
|
54
|
+
Requires-Dist: pyramid-tm ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
|
55
|
+
Requires-Dist: pyramid_mako ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
|
53
56
|
Requires-Dist: pyyaml
|
54
57
|
Requires-Dist: redis ; extra == "standard" or extra == "broadcast" or extra == "all"
|
55
58
|
Requires-Dist: requests
|
@@ -57,9 +60,9 @@ Requires-Dist: requests-oauthlib ; extra == "standard" or extra == "oauth2" or e
|
|
57
60
|
Requires-Dist: scikit-image ; extra == "test-images"
|
58
61
|
Requires-Dist: sentry-sdk ; extra == "standard" or extra == "sentry" or extra == "all"
|
59
62
|
Requires-Dist: ujson
|
60
|
-
Requires-Dist: waitress ; extra == "dev" or extra == "all"
|
61
|
-
Requires-Dist: zope.interface ; extra == "standard" or extra == "webserver" or extra == "all"
|
62
|
-
Requires-Dist: zope.sqlalchemy ; extra == "standard" or extra == "webserver" or extra == "all"
|
63
|
+
Requires-Dist: waitress ; extra == "standard" or extra == "dev" or extra == "waitress" or extra == "all"
|
64
|
+
Requires-Dist: zope.interface ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
|
65
|
+
Requires-Dist: zope.sqlalchemy ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
|
63
66
|
Project-URL: Repository, https://github.com/camptocamp/c2cwsgiutils
|
64
67
|
Description-Content-Type: text/markdown
|
65
68
|
|
@@ -673,6 +676,64 @@ def hello_get(request):
|
|
673
676
|
return {'hello': True}
|
674
677
|
```
|
675
678
|
|
679
|
+
## Waitress
|
680
|
+
|
681
|
+
In production mode we usually use Gunicorn but we can also use Waitress.
|
682
|
+
|
683
|
+
The advantage to use Waitress is that it creates only one process, that makes it easier to manage especially on Kubernetes:
|
684
|
+
|
685
|
+
- The memory is more stable.
|
686
|
+
- The OOM killer will restart the container.
|
687
|
+
- Prometheus didn't request trick to aggregate the metrics.
|
688
|
+
|
689
|
+
Then to migrate from Gunicorn to Waitress you should do:
|
690
|
+
|
691
|
+
Add call to `c2cwsgiutils.prometheus.start_single_process()` on your application main function.
|
692
|
+
|
693
|
+
Changes to do in your docker file:
|
694
|
+
|
695
|
+
```diff
|
696
|
+
|
697
|
+
ENV \
|
698
|
+
- GUNICORN_LOG_LEVEL=WARNING \
|
699
|
+
+ WAITRESS_LOG_LEVEL=WARNING \
|
700
|
+
+ WAITRESS_THREADS=10 \
|
701
|
+
|
702
|
+
-RUN mkdir -p /prometheus-metrics \
|
703
|
+
- && chmod a+rwx /prometheus-metrics
|
704
|
+
-ENV PROMETHEUS_MULTIPROC_DIR=/prometheus-metrics
|
705
|
+
|
706
|
+
|
707
|
+
-CMD ["/venv/bin/gunicorn", "--paste=/app/production.ini"]
|
708
|
+
+CMD ["/venv/bin/pserve", "c2c:///app/production.ini"]
|
709
|
+
```
|
710
|
+
|
711
|
+
Remove the no more needed file `gunicorn.conf.py`.
|
712
|
+
|
713
|
+
Update the `production.ini` file:
|
714
|
+
|
715
|
+
```diff
|
716
|
+
|
717
|
+
-# this file should be used by gunicorn.
|
718
|
+
|
719
|
+
[server:main]
|
720
|
+
+threads = %(WAITRESS_THREADS)s
|
721
|
+
+trusted_proxy = True
|
722
|
+
+clear_untrusted_proxy_headers = False
|
723
|
+
|
724
|
+
[loggers]
|
725
|
+
-keys = root, gunicorn, sqlalchemy, c2cwsgiutils, c2cwsgiutils_app
|
726
|
+
+keys = root, waitress, sqlalchemy, c2cwsgiutils, c2cwsgiutils_app
|
727
|
+
|
728
|
+
-[logger_gunicorn]
|
729
|
+
-level = %(GUNICORN_LOG_LEVEL)s
|
730
|
+
+[logger_waitress]
|
731
|
+
+level = %(WAITRESS_LOG_LEVEL)s
|
732
|
+
handlers =
|
733
|
+
-qualname = gunicorn.error
|
734
|
+
+qualname = waitress
|
735
|
+
```
|
736
|
+
|
676
737
|
# Exception handling
|
677
738
|
|
678
739
|
c2cwsgiutils can install exception handling views that will catch any exception raised by the
|
@@ -25,12 +25,12 @@ c2cwsgiutils/debug/utils.py,sha256=sIKZHQ8empzxE2OI3h7Uce8cvv4O7AD1y_VYeZfLVCA,2
|
|
25
25
|
c2cwsgiutils/errors.py,sha256=miNVnEX9qBtwaRnny_k9IcBbbw-8vC3unFumi2w5Q9w,6679
|
26
26
|
c2cwsgiutils/health_check.py,sha256=Wi3JQCKa7tqACuSgBi13oE69Xc-Vz1nheqBaxfOGXQU,20141
|
27
27
|
c2cwsgiutils/index.py,sha256=ag7A8HJ9MgU72etDBuFMaci-xKjOD9R8OOm3UMEoQJY,18002
|
28
|
-
c2cwsgiutils/loader.py,sha256=
|
28
|
+
c2cwsgiutils/loader.py,sha256=Hl9q-Ts-kXHLkV5PWFoO0jPBqhU2jKoCFvgDIdJaeF4,1333
|
29
29
|
c2cwsgiutils/logging_view.py,sha256=wa2hCjGKkpRwrCA4K6Zk1Z3hPGUMB6SJAnki-_lLY00,3384
|
30
30
|
c2cwsgiutils/models_graph.py,sha256=AROTzCHJCjjOiOmQj_Hk40WQdIPaHFLgN4Jech1aUzc,2683
|
31
31
|
c2cwsgiutils/pretty_json.py,sha256=houw_oeU63CgzQTPlBrrYzinpE-UtqPeYLfTXdLHyu0,1711
|
32
32
|
c2cwsgiutils/profiler.py,sha256=3902yp1rHpSNpsYKmqs_IrEpFg9P2QqNtjyolk53ZgQ,778
|
33
|
-
c2cwsgiutils/prometheus.py,sha256=
|
33
|
+
c2cwsgiutils/prometheus.py,sha256=vTb0cDO30DQwjVdu0qCFEyTAOv7jeUFioDjTCbp1bYA,8712
|
34
34
|
c2cwsgiutils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
35
35
|
c2cwsgiutils/pyramid.py,sha256=kSCsBG5IPd0-tsXwdiJ3cj_yj-AxQYFusvTxPE6AaeQ,1388
|
36
36
|
c2cwsgiutils/pyramid_logging.py,sha256=U1MD7xEAGH7QdX7FA6O12eHhzMx76i1WCXtf_EiJgVE,3911
|
@@ -44,7 +44,7 @@ c2cwsgiutils/scripts/stats_db.py,sha256=EaI1WAHGKj-sUWpznIOs8yMlgx2pnzwAksA8hA-n
|
|
44
44
|
c2cwsgiutils/scripts/test_print.py,sha256=pLqqVZ9cVvjdD3MdJQ7Ul8i5OIBFvo1Gnr78rdH0Dh4,2130
|
45
45
|
c2cwsgiutils/sentry.py,sha256=ZnWo0-8xgwWxDG_wo9gI8rDtZlKgcZXorG3b0tU5Hh0,6508
|
46
46
|
c2cwsgiutils/services.py,sha256=AxdiH2S8gfEQpOY36R4WqKtJ-3P4OXhx0srTm5G5070,1569
|
47
|
-
c2cwsgiutils/setup_process.py,sha256=
|
47
|
+
c2cwsgiutils/setup_process.py,sha256=K4o3feB98zKoPiI2k7MnTsItwf_i-NSL8ziUmGnYg_Q,3553
|
48
48
|
c2cwsgiutils/sql_profiler/__init__.py,sha256=YR8003lCNNGC2eZdyIt5gt76-1frG5szw8IYE_EsQB4,921
|
49
49
|
c2cwsgiutils/sql_profiler/_impl.py,sha256=OdhAn-5x84Bz5RCldhgjX50b6yxSLnLh5nydNgHvgGc,3801
|
50
50
|
c2cwsgiutils/sqlalchemylogger/README.md,sha256=qXfyhGC4Kbt-Ye_geTPm_1InpclhthmKZpc66rqB4as,2018
|
@@ -60,8 +60,8 @@ c2cwsgiutils/stats_pyramid/_db_spy.py,sha256=A61t6VKIrRRIjbyZTldmAUl_Q3ZDVFYqyxj
|
|
60
60
|
c2cwsgiutils/stats_pyramid/_pyramid_spy.py,sha256=GnJRlLSAPl4PwQH5jfmc2OX5Hz9MSRRVJT2VcqkniVE,3518
|
61
61
|
c2cwsgiutils/templates/index.html.mako,sha256=cK8qGBDeQG5SiJJCfvL0oKpgacr7dPKx634AAQivmjA,1416
|
62
62
|
c2cwsgiutils/version.py,sha256=7H3URblj26Ql0bL3eXtP0LSRBeW4HbEsQ8O_BfWNr90,3124
|
63
|
-
c2cwsgiutils-6.2.0.
|
64
|
-
c2cwsgiutils-6.2.0.
|
65
|
-
c2cwsgiutils-6.2.0.
|
66
|
-
c2cwsgiutils-6.2.0.
|
67
|
-
c2cwsgiutils-6.2.0.
|
63
|
+
c2cwsgiutils-6.2.0.dev41.dist-info/LICENSE,sha256=r7ueGz9Fl2Bv3rmeQy0DEtohLmAiufRaCuv6Y5fyNhE,1304
|
64
|
+
c2cwsgiutils-6.2.0.dev41.dist-info/METADATA,sha256=AUqcjIsz5x6TK9kL03Q2EOlR2dTLvoneGmYseJDVy0s,36359
|
65
|
+
c2cwsgiutils-6.2.0.dev41.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
66
|
+
c2cwsgiutils-6.2.0.dev41.dist-info/entry_points.txt,sha256=ujgqMTL1awN9qDg8WXmrF7m0fgR-hslUM6zKH86pvy0,703
|
67
|
+
c2cwsgiutils-6.2.0.dev41.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|