c2cwsgiutils 5.2.1__py3-none-any.whl → 5.2.1.dev197__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.
Files changed (56) hide show
  1. c2cwsgiutils/__init__.py +12 -12
  2. c2cwsgiutils/acceptance/connection.py +5 -2
  3. c2cwsgiutils/acceptance/image.py +95 -3
  4. c2cwsgiutils/acceptance/package-lock.json +1933 -0
  5. c2cwsgiutils/acceptance/package.json +7 -0
  6. c2cwsgiutils/acceptance/print.py +3 -3
  7. c2cwsgiutils/acceptance/screenshot.js +62 -0
  8. c2cwsgiutils/acceptance/utils.py +14 -22
  9. c2cwsgiutils/auth.py +4 -4
  10. c2cwsgiutils/broadcast/__init__.py +15 -7
  11. c2cwsgiutils/broadcast/interface.py +3 -2
  12. c2cwsgiutils/broadcast/local.py +3 -2
  13. c2cwsgiutils/broadcast/redis.py +6 -5
  14. c2cwsgiutils/client_info.py +5 -5
  15. c2cwsgiutils/config_utils.py +2 -1
  16. c2cwsgiutils/db.py +20 -11
  17. c2cwsgiutils/db_maintenance_view.py +2 -1
  18. c2cwsgiutils/debug/_listeners.py +7 -6
  19. c2cwsgiutils/debug/_views.py +11 -10
  20. c2cwsgiutils/debug/utils.py +5 -5
  21. c2cwsgiutils/health_check.py +72 -73
  22. c2cwsgiutils/index.py +90 -105
  23. c2cwsgiutils/loader.py +3 -3
  24. c2cwsgiutils/logging_view.py +3 -2
  25. c2cwsgiutils/models_graph.py +4 -4
  26. c2cwsgiutils/prometheus.py +175 -57
  27. c2cwsgiutils/pyramid.py +4 -2
  28. c2cwsgiutils/pyramid_logging.py +2 -1
  29. c2cwsgiutils/redis_stats.py +13 -11
  30. c2cwsgiutils/redis_utils.py +11 -5
  31. c2cwsgiutils/request_tracking/__init__.py +36 -30
  32. c2cwsgiutils/scripts/genversion.py +4 -4
  33. c2cwsgiutils/scripts/stats_db.py +92 -60
  34. c2cwsgiutils/sentry.py +2 -1
  35. c2cwsgiutils/setup_process.py +12 -16
  36. c2cwsgiutils/sql_profiler/_impl.py +3 -2
  37. c2cwsgiutils/sqlalchemylogger/_models.py +2 -2
  38. c2cwsgiutils/sqlalchemylogger/handlers.py +6 -6
  39. c2cwsgiutils/static/favicon-16x16.png +0 -0
  40. c2cwsgiutils/static/favicon-32x32.png +0 -0
  41. c2cwsgiutils/stats_pyramid/__init__.py +7 -11
  42. c2cwsgiutils/stats_pyramid/_db_spy.py +14 -11
  43. c2cwsgiutils/stats_pyramid/_pyramid_spy.py +27 -21
  44. c2cwsgiutils/templates/index.html.mako +50 -0
  45. c2cwsgiutils/version.py +49 -16
  46. {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/METADATA +168 -99
  47. c2cwsgiutils-5.2.1.dev197.dist-info/RECORD +67 -0
  48. {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/WHEEL +1 -1
  49. c2cwsgiutils/acceptance/composition.py +0 -129
  50. c2cwsgiutils/metrics.py +0 -110
  51. c2cwsgiutils/scripts/check_es.py +0 -130
  52. c2cwsgiutils/stats.py +0 -344
  53. c2cwsgiutils/stats_pyramid/_views.py +0 -16
  54. c2cwsgiutils-5.2.1.dist-info/RECORD +0 -66
  55. {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/LICENSE +0 -0
  56. {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/entry_points.txt +0 -0
c2cwsgiutils/version.py CHANGED
@@ -1,15 +1,38 @@
1
1
  import json
2
2
  import logging
3
3
  import os
4
+ import re
4
5
  import warnings
5
- from typing import Dict, Optional, cast
6
+ from typing import Optional, cast
6
7
 
8
+ import prometheus_client
7
9
  import pyramid.config
8
10
 
9
- from c2cwsgiutils import config_utils, stats
11
+ from c2cwsgiutils import config_utils, prometheus
10
12
 
11
- VERSIONS_PATH = "/app/versions.json"
12
- LOG = logging.getLogger(__name__)
13
+ _VERSIONS_PATH = "/app/versions.json"
14
+ _LOG = logging.getLogger(__name__)
15
+
16
+ _PACKAGES = os.environ.get("C2C_PROMETHEUS_PACKAGES", "c2cwsgiutils,pyramid,gunicorn,SQLAlchemy").split(",")
17
+ _APPLICATION_PACKAGES = os.environ.get("C2C_PROMETHEUS_APPLICATION_PACKAGE")
18
+ _LABEL_RE_NOT_ALLOWED = re.compile(r"[^a-zA-Z0-9]+")
19
+
20
+
21
+ def _sanitize_label(label: str) -> str:
22
+ # Replace chart that nor a-zA-Z0-9 with _
23
+ return _LABEL_RE_NOT_ALLOWED.sub("_", label)
24
+
25
+
26
+ _PROMETHEUS_VERSIONS_INFO = prometheus_client.Gauge(
27
+ prometheus.build_metric_name("version"),
28
+ "The version of the application",
29
+ labelnames=[
30
+ "git_hash",
31
+ *[_sanitize_label(p) for p in _PACKAGES],
32
+ *([] if _APPLICATION_PACKAGES is None else ["application"]),
33
+ ],
34
+ multiprocess_mode="liveall",
35
+ )
13
36
 
14
37
 
15
38
  def init(config: pyramid.config.Configurator) -> None:
@@ -20,7 +43,8 @@ def init(config: pyramid.config.Configurator) -> None:
20
43
 
21
44
  def includeme(config: pyramid.config.Configurator) -> None:
22
45
  """Initialize the versions view."""
23
- if os.path.isfile(VERSIONS_PATH):
46
+
47
+ if os.path.isfile(_VERSIONS_PATH):
24
48
  versions = _read_versions()
25
49
  config.add_route(
26
50
  "c2c_versions", config_utils.get_base_path(config) + r"/versions.json", request_method="GET"
@@ -28,30 +52,39 @@ def includeme(config: pyramid.config.Configurator) -> None:
28
52
  config.add_view(
29
53
  lambda request: versions, route_name="c2c_versions", renderer="fast_json", http_cache=0
30
54
  )
31
- LOG.info("Installed the /versions.json service")
55
+ _LOG.info("Installed the /versions.json service")
32
56
  git_hash = versions["main"]["git_hash"]
33
57
 
34
58
  if "git_tag" in versions["main"]:
35
- LOG.info("Starting version %s (%s)", versions["main"]["git_tag"], git_hash)
59
+ _LOG.info("Starting version %s (%s)", versions["main"]["git_tag"], git_hash)
36
60
  else:
37
- LOG.info("Starting version %s", git_hash)
61
+ _LOG.info("Starting version %s", git_hash)
38
62
 
39
- if stats.USE_TAGS:
40
- stats.increment_counter(["version"], 1, tags={"version": git_hash})
41
- else:
42
- stats.increment_counter(["version", git_hash], 1)
63
+ labels = {
64
+ "git_hash": git_hash,
65
+ **{
66
+ _sanitize_label(package): versions["packages"].get(package, "<missing>")
67
+ for package in _PACKAGES
68
+ },
69
+ **(
70
+ {}
71
+ if _APPLICATION_PACKAGES is None
72
+ else {"application": versions["packages"].get(_APPLICATION_PACKAGES, "<missing>")}
73
+ ),
74
+ }
75
+ _PROMETHEUS_VERSIONS_INFO.labels(**labels).set(1)
43
76
 
44
77
 
45
- def _read_versions() -> Dict[str, Dict[str, str]]:
78
+ def _read_versions() -> dict[str, dict[str, str]]:
46
79
  """Read the version."""
47
- with open(VERSIONS_PATH, encoding="utf-8") as file:
80
+ with open(_VERSIONS_PATH, encoding="utf-8") as file:
48
81
  versions = json.load(file)
49
- return cast(Dict[str, Dict[str, str]], versions)
82
+ return cast(dict[str, dict[str, str]], versions)
50
83
 
51
84
 
52
85
  def get_version() -> Optional[str]:
53
86
  """Get the version."""
54
- if not os.path.isfile(VERSIONS_PATH):
87
+ if not os.path.isfile(_VERSIONS_PATH):
55
88
  return None
56
89
  versions = _read_versions()
57
90
  return versions["main"]["git_hash"]
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: c2cwsgiutils
3
- Version: 5.2.1
3
+ Version: 5.2.1.dev197
4
4
  Summary: Common utilities for Camptocamp WSGI applications
5
5
  Home-page: https://github.com/camptocamp/c2cwsgiutils
6
6
  License: BSD-2-Clause
7
7
  Keywords: geo,gis,sqlalchemy,orm,wsgi
8
8
  Author: Camptocamp
9
9
  Author-email: info@camptocamp.com
10
- Requires-Python: >=3.8,<4.0
10
+ Requires-Python: >=3.9,<4.0
11
11
  Classifier: Development Status :: 5 - Production/Stable
12
12
  Classifier: Environment :: Plugins
13
13
  Classifier: Framework :: Pyramid
@@ -17,45 +17,48 @@ Classifier: License :: OSI Approved :: BSD License
17
17
  Classifier: Operating System :: OS Independent
18
18
  Classifier: Programming Language :: Python
19
19
  Classifier: Programming Language :: Python :: 3
20
- Classifier: Programming Language :: Python :: 3.8
21
20
  Classifier: Programming Language :: Python :: 3.9
22
21
  Classifier: Programming Language :: Python :: 3.10
23
22
  Classifier: Programming Language :: Python :: 3.11
24
- Classifier: Programming Language :: Python :: 3
25
- Classifier: Programming Language :: Python :: 3.8
26
23
  Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
27
24
  Classifier: Typing :: Typed
25
+ Provides-Extra: alembic
28
26
  Provides-Extra: all
29
27
  Provides-Extra: broadcast
28
+ Provides-Extra: debug
30
29
  Provides-Extra: dev
31
30
  Provides-Extra: oauth2
31
+ Provides-Extra: sentry
32
32
  Provides-Extra: standard
33
33
  Provides-Extra: test-images
34
- Requires-Dist: SQLAlchemy (>=1.4.0,<3.0.0) ; extra == "standard" or extra == "all"
35
- Requires-Dist: SQLAlchemy-Utils ; extra == "standard" or extra == "all"
36
- Requires-Dist: alembic ; extra == "standard" or extra == "all"
37
- Requires-Dist: boltons ; extra == "standard" or extra == "all"
38
- Requires-Dist: cee_syslog_handler ; extra == "standard" or extra == "all"
34
+ Provides-Extra: tests
35
+ Provides-Extra: webserver
36
+ Requires-Dist: SQLAlchemy (>=1.4.0,<3.0.0)
37
+ Requires-Dist: SQLAlchemy-Utils
38
+ Requires-Dist: alembic ; extra == "standard" or extra == "alembic" or extra == "all"
39
+ Requires-Dist: boltons ; extra == "tests" or extra == "all"
40
+ Requires-Dist: cee_syslog_handler
39
41
  Requires-Dist: certifi
40
- Requires-Dist: cornice ; extra == "standard" or extra == "all"
41
- Requires-Dist: gunicorn ; extra == "standard" or extra == "all"
42
- Requires-Dist: lxml ; extra == "standard" or extra == "all"
43
- Requires-Dist: netifaces ; extra == "standard" or extra == "all"
44
- Requires-Dist: objgraph ; extra == "standard" or extra == "all"
45
- Requires-Dist: psycopg2 ; extra == "standard" or extra == "all"
46
- Requires-Dist: pyjwt ; extra == "oauth2" or extra == "all"
47
- Requires-Dist: pyramid ; extra == "standard" or extra == "all"
48
- Requires-Dist: pyramid-tm ; extra == "standard" or extra == "all"
42
+ Requires-Dist: cornice
43
+ Requires-Dist: gunicorn ; extra == "standard" or extra == "webserver" or extra == "all"
44
+ Requires-Dist: lxml ; extra == "tests" or extra == "all"
45
+ Requires-Dist: objgraph ; extra == "debug" or extra == "all"
46
+ Requires-Dist: prometheus-client
47
+ Requires-Dist: psycopg2
48
+ Requires-Dist: pyjwt ; extra == "standard" or extra == "oauth2" or extra == "all"
49
+ Requires-Dist: pyramid
50
+ Requires-Dist: pyramid-tm
51
+ Requires-Dist: pyramid_mako
49
52
  Requires-Dist: pyyaml
50
- Requires-Dist: redis ; extra == "standard" or extra == "broadcast" or extra == "all" or extra == "all"
53
+ Requires-Dist: redis ; extra == "standard" or extra == "broadcast" or extra == "all"
51
54
  Requires-Dist: requests
52
- Requires-Dist: requests-oauthlib ; extra == "oauth2" or extra == "all"
55
+ Requires-Dist: requests-oauthlib ; extra == "standard" or extra == "oauth2" or extra == "all"
53
56
  Requires-Dist: scikit-image ; extra == "test-images"
54
- Requires-Dist: sentry-sdk ; extra == "standard" or extra == "all"
55
- Requires-Dist: ujson ; extra == "standard" or extra == "all"
57
+ Requires-Dist: sentry-sdk ; extra == "standard" or extra == "sentry" or extra == "all"
58
+ Requires-Dist: ujson
56
59
  Requires-Dist: waitress ; extra == "dev" or extra == "all"
57
- Requires-Dist: zope.interface ; extra == "standard" or extra == "all"
58
- Requires-Dist: zope.sqlalchemy ; extra == "standard" or extra == "all"
60
+ Requires-Dist: zope.interface
61
+ Requires-Dist: zope.sqlalchemy
59
62
  Project-URL: Repository, https://github.com/camptocamp/c2cwsgiutils
60
63
  Description-Content-Type: text/markdown
61
64
 
@@ -64,8 +67,7 @@ Description-Content-Type: text/markdown
64
67
  This is a Python 3 library (>=3.5) providing common tools for Camptocamp WSGI
65
68
  applications:
66
69
 
67
- - Provide a small framework for gathering performance statistics about
68
- a web application (statsd protocol)
70
+ - Provide prometheus metrics
69
71
  - Allow to use a master/slave PostgresQL configuration
70
72
  - Logging handler for CEE/UDP logs
71
73
  - An optional view to change runtime the log levels
@@ -104,7 +106,7 @@ You should install `c2cwsgiutils` with the tool you use to manage your pip depen
104
106
 
105
107
  In the `Dockerfile` you should add the following lines:
106
108
 
107
- ```
109
+ ```dockerfile
108
110
  # Generate the version file.
109
111
  RUN c2cwsgiutils-genversion $(git rev-parse HEAD)
110
112
 
@@ -112,20 +114,19 @@ CMD ["gunicorn", "--paste=/app/production.ini"]
112
114
 
113
115
  # Default values for the environment variables
114
116
  ENV \
115
- DEVELOPMENT=0 \
116
- SQLALCHEMY_POOL_RECYCLE=30 \
117
- SQLALCHEMY_POOL_SIZE=5 \
118
- SQLALCHEMY_MAX_OVERFLOW=25 \
119
- SQLALCHEMY_SLAVE_POOL_RECYCLE=30 \
120
- SQLALCHEMY_SLAVE_POOL_SIZE=5 \
121
- SQLALCHEMY_SLAVE_MAX_OVERFLOW=25\
122
- LOG_TYPE=console \
123
- OTHER_LOG_LEVEL=WARNING \
124
- GUNICORN_LOG_LEVEL=WARNING \
125
- GUNICORN_ACCESS_LOG_LEVEL=INFO \
126
- SQL_LOG_LEVEL=WARNING \
127
- C2CWSGIUTILS_LOG_LEVEL=WARNING \
128
- LOG_LEVEL=INFO
117
+ DEVELOPMENT=0 \
118
+ SQLALCHEMY_POOL_RECYCLE=30 \
119
+ SQLALCHEMY_POOL_SIZE=5 \
120
+ SQLALCHEMY_MAX_OVERFLOW=25 \
121
+ SQLALCHEMY_SLAVE_POOL_RECYCLE=30 \
122
+ SQLALCHEMY_SLAVE_POOL_SIZE=5 \
123
+ SQLALCHEMY_SLAVE_MAX_OVERFLOW=25\
124
+ LOG_TYPE=console \
125
+ OTHER_LOG_LEVEL=WARNING \
126
+ GUNICORN_LOG_LEVEL=WARNING \
127
+ SQL_LOG_LEVEL=WARNING \
128
+ C2CWSGIUTILS_LOG_LEVEL=WARNING \
129
+ LOG_LEVEL=INFO
129
130
  ```
130
131
 
131
132
  Add in your `main` function.
@@ -161,8 +162,6 @@ The related environment variables:
161
162
  - `SQL_LOG_LEVEL`: The SQL query log level, `WARNING`: no logs, `INFO`: logs the queries,
162
163
  `DEBUG` also logs the results, default is `WARNING`.
163
164
  - `GUNICORN_ERROR_LOG_LEVEL`: The Gunicorn error log level, default is `WARNING`.
164
- - `GUNICORN_ACCESS_LOG_LEVEL`: The Gunicorn access log level, the logs have the level `INFO`,
165
- default is `WARNING`.
166
165
  - `C2CWSGIUTILS_CONFIG`: The fallback ini file to use by gunicorn, default is `production.ini`.
167
166
  - `C2CWSGIUTILS_LOG_LEVEL`: The c2c WSGI utils log level, default is `WARNING`.
168
167
  - `OTHER_LOG_LEVEL`: The log level for all the other logger, default is `WARNING`.
@@ -331,47 +330,6 @@ The requests module is also patched to monitor requests done without timeout. In
331
330
  configure a default timeout with the `C2C_REQUESTS_DEFAULT_TIMEOUT` environment variable
332
331
  (`c2c.requests_default_timeout`). If no timeout and no default is specified, a warning is issued.
333
332
 
334
- ## Metrics
335
-
336
- To enable and configure the metrics framework, you can use:
337
-
338
- - STATS_VIEW (c2c.stats_view): if defined, will enable the stats view `{C2C_BASE_PATH}/stats.json`
339
- - STATSD_ADDRESS (c2c.statsd_address): if defined, send stats to the given statsd server
340
- - STATSD_PREFIX (c2c.statsd_prefix): prefix to add to every metric names
341
- - STATSD_USE_TAGS: If true, automatic metrics will use tags
342
- - STATSD*TAG*{tag_name}: To set a global tag for the service
343
-
344
- If enabled, some metrics are automatically generated:
345
-
346
- - {STATSD_PREFIX}.route.{verb}.{route_name}.{status}: The time to process a query (includes rendering)
347
- - {STATSD_PREFIX}.render.{verb}.{route_name}.{status}: The time to render a query
348
- - {STATSD_PREFIX}.sql.{query}: The time to execute the given SQL query (simplified and normalized)
349
- - {STATSD_PREFIX}.requests.{scheme}.{hostname}.{port}.{verb}.{status}: The time to execute HTTP requests to
350
- outside services (only the time between the start of sending of the request and when the header is
351
- back with a chunk of the body)
352
- - {STATSD_PREFIX}.redis.{command}: The time to execute the given Redis command
353
-
354
- You can manually measure the time spent on something like that:
355
-
356
- ```python
357
- from c2cwsgiutils import stats
358
- with stats.timer_context(['toto', 'tutu']):
359
- do_something()
360
- ```
361
-
362
- It will only add a timer event in case of success. If you want to measure both success and failures, do that:
363
-
364
- ```python
365
- from c2cwsgiutils import stats
366
- with stats.outcome_timer_context(['toto', 'tutu']):
367
- do_something()
368
- ```
369
-
370
- Other functions exists to generate metrics. Look at the `c2cwsgiutils.stats` module.
371
-
372
- Look at the `c2cwsgiutils-stats-db` utility if you want to generate statistics (gauges) about the
373
- row counts.
374
-
375
333
  ## SQL profiler
376
334
 
377
335
  The SQL profiler must be configured with the `C2C_SQL_PROFILER_ENABLED` environment variable. That enables a view
@@ -519,27 +477,100 @@ If the `/app/versions.json` exists, a view is added (`{C2C_BASE_PATH}/versions.j
519
477
  version of a app. This file is generated by calling the `c2cwsgiutils-genversion [$GIT_TAG] $GIT_HASH`
520
478
  command line. Usually done in the [Dockerfile](acceptance_tests/app/Dockerfile) of the WSGI application.
521
479
 
522
- ## Metrics
480
+ ## Prometheus
523
481
 
524
- The path `/metrics` provide some metrics for Prometheus.
525
- By default we have the `smap` `pss`, but we can easily add the `rss`, `size` or your custom settings:
482
+ [Prometheus client](https://github.com/prometheus/client_python) is integrated in c2cwsgiutils.
526
483
 
527
- Example:
484
+ It will work in multi process mode with the limitation listed in the
485
+ [`prometheus_client` documentation](https://github.com/prometheus/client_python#multiprocess-mode-eg-gunicorn).
528
486
 
487
+ To enable it you should provide the `C2CWSGIUTILS_PROMETHEUS_PORT` environment variable.
488
+ For security reason, this port should not be exposed.
489
+
490
+ We can customize it with the following environment variables:
491
+
492
+ - `C2C_PROMETHEUS_PREFIX`: to customize the prefix, default is `c2cwsggiutils-`.
493
+ - `C2C_PROMETHEUS_PACKAGES` the packages that will be present in the version information, default is `c2cwsgiutils,pyramid,gunicorn,sqlalchemy`.
494
+ - `C2C_PROMETHEUS_APPLICATION_PACKAGE` the packages that will be present in the version information as application.
495
+
496
+ And you should add in your `gunicorn.conf.py`:
497
+
498
+ ```python
499
+ from prometheus_client import multiprocess
500
+
501
+
502
+ def on_starting(server):
503
+ from c2cwsgiutils import prometheus
504
+
505
+ del server
506
+
507
+ prometheus.start()
508
+
509
+
510
+ def post_fork(server, worker):
511
+ from c2cwsgiutils import prometheus
512
+
513
+ del server, worker
514
+
515
+ prometheus.cleanup()
516
+
517
+
518
+ def child_exit(server, worker):
519
+ del server
520
+
521
+ multiprocess.mark_process_dead(worker.pid)
529
522
  ```
530
- from import c2cwsgiutils.metrics import add_provider, Provider, MemoryMapProvider
531
523
 
532
- class CustomProvider(Provider):
533
- def __init__(self):
534
- super().__init__("my_metrics", "My Metric")
524
+ In your `Dockerfile` you should add:
525
+
526
+ ```dockerfile
527
+ RUN mkdir -p /prometheus-metrics \
528
+ && chmod a+rwx /prometheus-metrics
529
+ ENV PROMETHEUS_MULTIPROC_DIR=/prometheus-metrics
530
+ ```
535
531
 
536
- def get_data(self):
537
- return [({'metadata_key': 'matadata_value'}, metrics_value)]
532
+ ### Add custom metric collector
538
533
 
539
- add_provider(MemoryMapProvider('rss'))
540
- add_provider(CustomProvider())
534
+ See [official documentation](https://github.com/prometheus/client_python#custom-collectors).
535
+
536
+ Related to the Unix process.
537
+
538
+ ```python
539
+ from c2cwsgiutils import broadcast, prometheus
540
+
541
+ prometheus.MULTI_PROCESS_COLLECTOR_BROADCAST_CHANNELS.append("prometheus_collector_custom")
542
+ broadcast.subscribe("c2cwsgiutils_prometheus_collect_gc", _broadcast_collector_custom)
543
+ my_custom_collector_instance = MyCustomCollector()
544
+
545
+
546
+ def _broadcast_collector_custom() -> List[prometheus.SerializedGauge]:
547
+ """Get the collected GC gauges."""
548
+
549
+ return prometheus.serialize_collected_data(my_custom_collector_instance)
550
+ ```
551
+
552
+ Related to the host, use that in the `gunicorn.conf.py`:
553
+
554
+ ```python
555
+ def on_starting(server):
556
+ from c2cwsgiutils import prometheus
557
+
558
+ del server
559
+
560
+ registry = CollectorRegistry()
561
+ registry.register(MyCollector())
562
+ prometheus.start(registry)
541
563
  ```
542
564
 
565
+ ### Database metrics
566
+
567
+ Look at the `c2cwsgiutils-stats-db` utility if you want to generate statistics (gauges) about the
568
+ row counts.
569
+
570
+ ### Usage of metrics
571
+
572
+ With c2cwsgiutils each instance (Pod) has its own metrics, so we need to aggregate them to have the metrics for the service you probably need to use `sum by (<fields>) (<metric>)` to get the metric (especially for counters).
573
+
543
574
  ## Custom scripts
544
575
 
545
576
  To have the application initialized in a script you should use the
@@ -592,7 +623,7 @@ have dumps of a few things:
592
623
  - memory usage: `{C2C_BASE_PATH}/debug/memory?secret={C2C_SECRET}&limit=30&analyze_type=builtins.dict&python_internals_map=false`
593
624
  - object ref: `{C2C_BASE_PATH}/debug/show_refs.dot?secret={C2C_SECRET}&analyze_type=gunicorn.app.wsgiapp.WSGIApplication&analyze_id=12345&max_depth=3&too_many=10&filter=1024&no_extra_info&backrefs`
594
625
  `analyze_type` and `analyze_id` should not ve used toogether, you can use it like:
595
- ```
626
+ ```bash
596
627
  curl "<URL>" > /tmp/show_refs.dot
597
628
  dot -Lg -Tpng /tmp/show_refs.dot > /tmp/show_refs.png
598
629
  ```
@@ -653,7 +684,7 @@ client. In production mode, you can still get them by sending the secret defined
653
684
 
654
685
  If you want to use pyramid_debugtoolbar, you need to disable exception handling and configure it like that:
655
686
 
656
- ```
687
+ ```ini
657
688
  pyramid.includes =
658
689
  pyramid_debugtoolbar
659
690
  debugtoolbar.enabled = true
@@ -727,3 +758,41 @@ To make a release:
727
758
  - Add the new branch name in the `.github/workflows/rebuild.yaml` and
728
759
  `.github/workflows/audit.yaml` files.
729
760
 
761
+ ## Testing
762
+
763
+ ### Screenshots
764
+
765
+ To test the screenshots, you need to install `node` with `npm`, to do that add the following lines in your `Dockerfile`:
766
+
767
+ ```dockerfile
768
+ RUN --mount=type=cache,target=/var/lib/apt/lists \
769
+ --mount=type=cache,target=/var/cache,sharing=locked \
770
+ . /etc/os-release \
771
+ && echo "deb https://deb.nodesource.com/node_18.x ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/nodesource.list \
772
+ && curl --silent https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - \
773
+ && apt-get update \
774
+ && apt-get install --assume-yes --no-install-recommends 'nodejs=18.*'
775
+ ```
776
+
777
+ To do the image test call `check_screenshot` e.g.:
778
+
779
+ ```python
780
+ def test_screenshot(app_connection):
781
+ image.check_screenshot(
782
+ app_connection.base_url + "my-path",
783
+ width=800,
784
+ height=600,
785
+ result_folder="results",
786
+ expected_filename=os.path.join(os.path.dirname(__file__), "my-check.expected.png"),
787
+ )
788
+ ```
789
+
790
+ ## Contributing
791
+
792
+ Install the pre-commit hooks:
793
+
794
+ ```bash
795
+ pip install pre-commit
796
+ pre-commit install --allow-missing-config
797
+ ```
798
+
@@ -0,0 +1,67 @@
1
+ c2cwsgiutils/__init__.py,sha256=YHZ6OY8BvFrZAoBpbEWmAZ-60nqDer_CKHgY-LSHAKs,3986
2
+ c2cwsgiutils/acceptance/__init__.py,sha256=vjtpPfu0kbXUOYMx15Z8713IfPFZA9XnkUKkIFtVj_M,1500
3
+ c2cwsgiutils/acceptance/connection.py,sha256=WKD4yDDvOqpKTcwCn9cIK5W_AWxumGboe8Sel7ZeXHI,9146
4
+ c2cwsgiutils/acceptance/image.py,sha256=_LNKuUYnljZTyPoVqtOLvWb5HWFr_7mQXWQh2ow4o4Q,7200
5
+ c2cwsgiutils/acceptance/package-lock.json,sha256=Y50ESiYKsdaZsmTo448ScQa1Vrn8DnuDgpuEkSXSHIE,76668
6
+ c2cwsgiutils/acceptance/package.json,sha256=nQN96fhokGljwDhBoLn00rm9jSd7_U38qRs44oHY394,101
7
+ c2cwsgiutils/acceptance/print.py,sha256=j5K1c2Kn0eEnhgdbZNBVkdscK02pQhtPIh6lJzHMJcM,2323
8
+ c2cwsgiutils/acceptance/screenshot.js,sha256=pp8G5Bb04aDb1VX7PnLwBxVhaxN9p5PUU3iPWENVGoI,1899
9
+ c2cwsgiutils/acceptance/utils.py,sha256=-NgLlG_oQj3P_ZiK293RG7ZHPumg0WrDwo_APOI3WG4,1851
10
+ c2cwsgiutils/auth.py,sha256=73NkFu4mKeDwulECRabSFhpNUKoEfP-0M3JnTQ9lJ3s,9333
11
+ c2cwsgiutils/broadcast/__init__.py,sha256=Ae0qU6nP0G6Qa6Zi-vJK4Om8T9HKxmaz5-uOAJKZZdE,4360
12
+ c2cwsgiutils/broadcast/interface.py,sha256=jE8BSy9N7xnPmq5U0m852sFFhx46c7Uo9SyFJTCde9o,636
13
+ c2cwsgiutils/broadcast/local.py,sha256=24aIRdFOR2CXZfp_F2R_S1QW-yon3EyTM6TvljWVlP0,1083
14
+ c2cwsgiutils/broadcast/redis.py,sha256=sxBsYZtMNXMOck6a_Mcb3QZjjW4lPTxnImTfL1vv14g,5085
15
+ c2cwsgiutils/broadcast/utils.py,sha256=0fQZXPu3p_5LEJpGenJwiiMxECQjJhjZBjIkBk8h-ng,272
16
+ c2cwsgiutils/client_info.py,sha256=t35d8yI6MVDoB3i2yRKk0k-GmLP3v-WCis5JVzC6XFg,3044
17
+ c2cwsgiutils/config_utils.py,sha256=N_DPNRCBmeomhv2cJN5OQALrTAG29FCCk3VeugWrBwI,1523
18
+ c2cwsgiutils/coverage_setup.py,sha256=fES0sdhFy6oaeOCuP1qjjm7PQL9l_O8rUKZhRvRBRwQ,839
19
+ c2cwsgiutils/db.py,sha256=N_9zxxXYKiKwfNxs5KsunKYb_gUZQkTrlw6HZu5qsgQ,16174
20
+ c2cwsgiutils/db_maintenance_view.py,sha256=ejSNCv7vZIDjXctDTWTXEYTnUYFVVNIecFLeDlCkDBA,3076
21
+ c2cwsgiutils/debug/__init__.py,sha256=80zdAZnE9cwgQW1odE2aOauIxYsG5CQpWvHPcslRue8,1239
22
+ c2cwsgiutils/debug/_listeners.py,sha256=sXXHbPHQaSRMrzUC2ryiSDsBXuTdVbpQOxmMmhZ3n90,4378
23
+ c2cwsgiutils/debug/_views.py,sha256=zUeIshxBshBrlTz0p1I1LUi6HiJBOAAiDPrlOJsziQA,7522
24
+ c2cwsgiutils/debug/utils.py,sha256=TPlJC5qKeFnvbgq1xjlfrrRgDcV5kIR69IPJgNcIZQY,2311
25
+ c2cwsgiutils/errors.py,sha256=LUhZF3BW1i-v20x3EE-rZbT-TpdugpxiW7p5iCAu73Q,6723
26
+ c2cwsgiutils/health_check.py,sha256=O-GGS0aicTuMz4gqE-btk8ipzVi5allDe6sFuyOANOA,19977
27
+ c2cwsgiutils/index.py,sha256=jFThEi1sbsd2RnMNwhXpVAQUcpZSxW7ava9kxYwO5Zk,16703
28
+ c2cwsgiutils/loader.py,sha256=x_yHRTDzzlQ61fHonWnnG01xdqFuXpbGZMNN--tN25U,622
29
+ c2cwsgiutils/logging_view.py,sha256=W6-dFTz00hLt6BGJ8V3-4ip4AdxTyPG2W5vQjIuKiQs,3357
30
+ c2cwsgiutils/models_graph.py,sha256=laip8EdhI2hoGZVAotdrsgMwiNbwsJPjknKkRq1eEq0,2680
31
+ c2cwsgiutils/pretty_json.py,sha256=f1-oecFX9hub1nD32mmZRjOTIxhV8bVSt3Meqw68sNU,1698
32
+ c2cwsgiutils/profiler.py,sha256=3tIwoDSzOKQ06ug_U6j5VDR1BQ9auUOqdJRRLRhDoHw,739
33
+ c2cwsgiutils/prometheus.py,sha256=a09eTEmmhv_WENYx5I8jZUFdbUuVztE_lbMdOJNDm1U,6494
34
+ c2cwsgiutils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
+ c2cwsgiutils/pyramid.py,sha256=H-b5abZvxt9MkMFjyWOjWzPa02YRhZR6LDiMCaYLq5s,1388
36
+ c2cwsgiutils/pyramid_logging.py,sha256=vVPrkAKZoIwb6SPf-omZDGyiKfElBf5yo_dPaIo-8ko,3730
37
+ c2cwsgiutils/redis_stats.py,sha256=triLtzryGXJKvCUw4TreYpF22BpUKPdrMp30ZGKsXwU,1545
38
+ c2cwsgiutils/redis_utils.py,sha256=dSe7qvnobCglKwDbYg4OwCxWxWg4migxmHbgClU2Qnw,4630
39
+ c2cwsgiutils/request_tracking/__init__.py,sha256=5SxMqBV3_w6SyykB1FFKug7XqzwdPhQdQOZ4VKUM4A8,4039
40
+ c2cwsgiutils/request_tracking/_sql.py,sha256=ME13tdXV2Vvhvscon9DbDUqehNWcn4uL75uqzcN5-Mg,577
41
+ c2cwsgiutils/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
+ c2cwsgiutils/scripts/genversion.py,sha256=GKmD08-H83hiKqXxy4odABuWqD5gADs_YEzM3TzHxEA,1985
43
+ c2cwsgiutils/scripts/stats_db.py,sha256=QNwtEKznCgOKg4ZsAFED9ISS-THgdwD2km_oVoII5XU,10317
44
+ c2cwsgiutils/scripts/test_print.py,sha256=UeOZa7jTazgEq5BRJD6lq-u9K6G4movf-sOVKTEs1cQ,2096
45
+ c2cwsgiutils/sentry.py,sha256=X-8Pd9wEi6EKMde50LxcdeM6LYBRyGnJpkPgEmFgqpg,5029
46
+ c2cwsgiutils/services.py,sha256=qz51oCZOC0Lj2_ig4UuHIm0ZZO3FfpFTxrXBWZ_oaNo,1567
47
+ c2cwsgiutils/setup_process.py,sha256=RKZGQdcKvHh_YU2mA-J5FQ01XNGj-SevcPNQB0439rs,3428
48
+ c2cwsgiutils/sql_profiler/__init__.py,sha256=lZYq83LYlm_P4uNMv0WU_B9Obl90YaNzkqWtteUHadg,876
49
+ c2cwsgiutils/sql_profiler/_impl.py,sha256=0UWSZDEm2E9Zeujd8UFRu0cIyyHBMxXxdmzt0cmuv0s,3702
50
+ c2cwsgiutils/sqlalchemylogger/README.md,sha256=WEyJSrBjedtX1FFrYiq4oMaWMt1fNxRkJYmJWnAoz3g,1552
51
+ c2cwsgiutils/sqlalchemylogger/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
+ c2cwsgiutils/sqlalchemylogger/_filters.py,sha256=OJQ9_WA-fd9fMZ7TUNFzHHTPI6msw2NVBl5RoeYFnGw,752
53
+ c2cwsgiutils/sqlalchemylogger/_models.py,sha256=A9SQ8AqUazCMemVjp5p_1x4bZG3LAYW9pOXT84FdNkE,1471
54
+ c2cwsgiutils/sqlalchemylogger/examples/example.py,sha256=n48dJdUi1FH1hfBMAbfHLGPSb1bOVD8pXMxXB57PnpQ,460
55
+ c2cwsgiutils/sqlalchemylogger/handlers.py,sha256=Qul-Bebmxqt4KvFU4TqjUGnbmpAVBviF5pSV3duUqG0,4801
56
+ c2cwsgiutils/static/favicon-16x16.png,sha256=LKk6RFvb3NlPIZdDfAodE8H9IN8KM6CMGnMx4vOHlUQ,887
57
+ c2cwsgiutils/static/favicon-32x32.png,sha256=i4ucx08zAZARd8e7JTMGK-gb5WcOmyuDN6IN4brsEOo,1216
58
+ c2cwsgiutils/stats_pyramid/__init__.py,sha256=7P10LjLv3c-ObEDGuYmRF_RFt7fRmO80ruqTGQAyC6w,747
59
+ c2cwsgiutils/stats_pyramid/_db_spy.py,sha256=ZGRdrI17Bdl3mzaLjfPyAaEW3KK8Pikrgi-0WmH7zCs,2917
60
+ c2cwsgiutils/stats_pyramid/_pyramid_spy.py,sha256=P212MGGl2VV_7UU4AXZA-rOuF7ouaONRklZwpas2wc8,3209
61
+ c2cwsgiutils/templates/index.html.mako,sha256=zrdMxncl3PFwdg1TVDKAjH1CaXw1m7yr9dj8Hi7rYGw,1362
62
+ c2cwsgiutils/version.py,sha256=z4of1DDr6J7PDw4AUOz31Gp63khgXf3JfiIaoWUM-9I,2870
63
+ c2cwsgiutils-5.2.1.dev197.dist-info/LICENSE,sha256=rM6IWxociA3daRkXnNLYOxGndT5fbs3BfVZCA2Xgt-g,1304
64
+ c2cwsgiutils-5.2.1.dev197.dist-info/METADATA,sha256=AXKlHCfO4_Uq--GCt6EpxznwArBUXtSO-IQrrlH-CH8,32515
65
+ c2cwsgiutils-5.2.1.dev197.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
66
+ c2cwsgiutils-5.2.1.dev197.dist-info/entry_points.txt,sha256=ujgqMTL1awN9qDg8WXmrF7m0fgR-hslUM6zKH86pvy0,703
67
+ c2cwsgiutils-5.2.1.dev197.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.5.0
2
+ Generator: poetry-core 1.7.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,129 +0,0 @@
1
- import logging
2
- import os
3
- import subprocess # nosec
4
- import sys
5
- import time
6
- import warnings
7
- from typing import Any, Callable, Dict, List, Mapping, Optional, cast
8
-
9
- import netifaces
10
- from pyramid.request import Request
11
-
12
- from c2cwsgiutils.acceptance import utils
13
-
14
- LOG = logging.getLogger(__name__)
15
- logging.basicConfig(
16
- level=logging.DEBUG,
17
- format="TEST: %(asctime)-15s %(levelname)5s %(name)s %(message)s",
18
- stream=sys.stdout,
19
- )
20
- logging.getLogger("requests.packages.urllib3.connectionpool").setLevel(logging.WARN)
21
-
22
-
23
- def _try(what: Callable[[], Any], fail: bool = True, times: int = 5, delay: int = 10) -> Optional[Any]:
24
- for i in range(times):
25
- try:
26
- return what()
27
- except: # pylint: disable=bare-except
28
- LOG.warning("Exception:", exc_info=True)
29
- if i + 1 == times and fail:
30
- raise
31
- time.sleep(delay)
32
- return None
33
-
34
-
35
- class Composition:
36
- """The Docker composition."""
37
-
38
- def __init__(self, request: Request, composition: str, coverage_paths: Optional[str] = None) -> None:
39
- warnings.warn("The c2cwsgiutils.acceptance.composition should be used only if it's relay needed.")
40
- self.cwd = os.path.dirname(composition)
41
- filename = os.path.basename(composition)
42
- self.docker_compose = ["docker-compose"]
43
- if filename != "docker-compose.yaml":
44
- self.docker_compose.append("--file=" + filename)
45
- self.coverage_paths = coverage_paths
46
- env = Composition._get_env()
47
- if os.environ.get("docker_start", "1") == "1":
48
- self.dc_try(["stop"], fail=False)
49
- self.dc_try(["rm", "-f"], fail=False)
50
- self.dc_try(["build"], fail=False)
51
- self.dc_try(["up", "-d"], fail=False)
52
-
53
- # Setup something that redirects the docker container logs to the test output
54
- log_watcher = subprocess.Popen( # nosec, pylint: disable=consider-using-with
55
- self.docker_compose + ["logs", "--follow", "--no-color"],
56
- cwd=self.cwd,
57
- env=env,
58
- stderr=subprocess.STDOUT,
59
- )
60
- request.addfinalizer(log_watcher.kill)
61
- if os.environ.get("docker_stop", "1") == "1":
62
- request.addfinalizer(self.stop_all)
63
-
64
- def dc(self, args: List[str], **kwargs: Any) -> str:
65
- return cast(
66
- str,
67
- subprocess.check_output( # nosec
68
- self.docker_compose + args,
69
- cwd=self.cwd,
70
- env=Composition._get_env(),
71
- stderr=subprocess.STDOUT,
72
- **kwargs,
73
- ).decode(),
74
- )
75
-
76
- def dc_try(self, args: List[str], **kwargs: Any) -> None:
77
- _try(
78
- lambda: self.dc(args),
79
- **kwargs,
80
- )
81
-
82
- def stop_all(self) -> None:
83
- self.dc_try(["stop"])
84
- if self.coverage_paths:
85
- target_dir = "/reports/"
86
- os.makedirs(target_dir, exist_ok=True)
87
- for path in self.coverage_paths:
88
- try:
89
- subprocess.check_call( # nosec
90
- ["docker", "cp", path, target_dir], stderr=subprocess.STDOUT
91
- )
92
- except Exception:
93
- self.dc(["ps"])
94
- raise
95
-
96
- def stop(self, container: str) -> None:
97
- self.dc_try(["stop", container])
98
-
99
- def restart(self, container: str) -> None:
100
- self.dc_try(["restart", container])
101
-
102
- def run(self, container: str, *command: str, **kwargs: Dict[str, Any]) -> str:
103
- return self.dc(
104
- ["run", "--rm", container] + list(command),
105
- **kwargs,
106
- )
107
-
108
- def exec(self, container: str, *command: str, **kwargs: Dict[str, Any]) -> str:
109
- return self.dc(["exec", "-T", container] + list(command), **kwargs)
110
-
111
- @staticmethod
112
- def _get_env() -> Mapping[str, str]:
113
- """
114
- Make sure the DOCKER_TAG environment variable.
115
-
116
- Used in the docker-compose.yaml file is correctly set when we call docker-compose.
117
- """
118
- env = dict(os.environ)
119
- if "DOCKER_TAG" not in env:
120
- env["DOCKER_TAG"] = "latest"
121
- if utils.in_docker():
122
- env["DOCKER_IP"] = netifaces.gateways()[netifaces.AF_INET][0][0]
123
- env["DOCKER_CB_HOST"] = env["DOCKER_IP"]
124
- else:
125
- env["DOCKER_IP"] = netifaces.ifaddresses("docker0")[netifaces.AF_INET][0]["addr"]
126
- env["DOCKER_CB_HOST"] = "localhost"
127
- default_iface = netifaces.gateways()[netifaces.AF_INET][0][1]
128
- env["TEST_IP"] = netifaces.ifaddresses(default_iface)[netifaces.AF_INET][0]["addr"]
129
- return env