c2cwsgiutils 6.2.0.dev36__py3-none-any.whl → 6.2.0.dev39__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 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()
@@ -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
@@ -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.dev36
3
+ Version: 6.2.0.dev39
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: SQLAlchemy ; extra == "standard" or extra == "webserver" or extra == "all"
38
- Requires-Dist: SQLAlchemy-Utils ; extra == "standard" or extra == "webserver" or extra == "all"
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=vU7yEobl9TlSFZwdLI2eF1ceUJ6xgiu2re31HA5sw1g,623
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=XIVnsI9zcIZsg0-tqrDSiHClDwdr3l5KMkmpFKyfUXk,6635
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=19p-0ZRZQ08S6-hkZvAq_VJjtrLeWxsdoX-afRLJapg,3454
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.dev36.dist-info/LICENSE,sha256=r7ueGz9Fl2Bv3rmeQy0DEtohLmAiufRaCuv6Y5fyNhE,1304
64
- c2cwsgiutils-6.2.0.dev36.dist-info/METADATA,sha256=s18TzIHXyoYwmI7W0eKRRh2r9Ed4DNMxAUAntveyU20,34461
65
- c2cwsgiutils-6.2.0.dev36.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
66
- c2cwsgiutils-6.2.0.dev36.dist-info/entry_points.txt,sha256=ujgqMTL1awN9qDg8WXmrF7m0fgR-hslUM6zKH86pvy0,703
67
- c2cwsgiutils-6.2.0.dev36.dist-info/RECORD,,
63
+ c2cwsgiutils-6.2.0.dev39.dist-info/LICENSE,sha256=r7ueGz9Fl2Bv3rmeQy0DEtohLmAiufRaCuv6Y5fyNhE,1304
64
+ c2cwsgiutils-6.2.0.dev39.dist-info/METADATA,sha256=E3o2wdqpTiwsZqHf5FX6mzBaXeIQQruwiYS7nHwjJis,36359
65
+ c2cwsgiutils-6.2.0.dev39.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
66
+ c2cwsgiutils-6.2.0.dev39.dist-info/entry_points.txt,sha256=ujgqMTL1awN9qDg8WXmrF7m0fgR-hslUM6zKH86pvy0,703
67
+ c2cwsgiutils-6.2.0.dev39.dist-info/RECORD,,