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
@@ -12,11 +12,12 @@ import re
12
12
  import subprocess # nosec
13
13
  import time
14
14
  import traceback
15
- from collections import Counter
15
+ from collections.abc import Mapping
16
16
  from enum import Enum
17
17
  from types import TracebackType
18
- from typing import Any, Callable, Dict, List, Literal, Mapping, Optional, Tuple, Type, Union, cast
18
+ from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, Union, cast
19
19
 
20
+ import prometheus_client
20
21
  import pyramid.config
21
22
  import pyramid.request
22
23
  import requests
@@ -26,11 +27,35 @@ import sqlalchemy.sql
26
27
  from pyramid.httpexceptions import HTTPNotFound
27
28
 
28
29
  import c2cwsgiutils.db
29
- from c2cwsgiutils import auth, broadcast, config_utils, redis_utils, stats, version
30
+ from c2cwsgiutils import auth, broadcast, config_utils, prometheus, redis_utils, version
31
+
32
+ if TYPE_CHECKING:
33
+ scoped_session = sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]
34
+ else:
35
+ scoped_session = sqlalchemy.orm.scoped_session
30
36
 
31
37
  LOG = logging.getLogger(__name__)
32
38
  ALEMBIC_HEAD_RE = re.compile(r"^([a-f0-9]+) \(head\)\n$")
33
39
 
40
+ _PROMETHEUS_DB_SUMMARY = prometheus_client.Summary(
41
+ prometheus.build_metric_name("health_check_db"),
42
+ "The to do a database query",
43
+ ["configuration", "connection", "check"],
44
+ unit="seconds",
45
+ )
46
+ _PROMETHEUS_ALEMBIC_VERSION = prometheus_client.Gauge(
47
+ prometheus.build_metric_name("alembic_version"),
48
+ "The alembic version of the database",
49
+ ["version", "name", "configuration"],
50
+ multiprocess_mode="liveall",
51
+ )
52
+ _PROMETHEUS_HEALTH_CHECKS_FAILURE = prometheus_client.Gauge(
53
+ prometheus.build_metric_name("health_check_failure"),
54
+ "The health check",
55
+ ["name"],
56
+ multiprocess_mode="livemax",
57
+ )
58
+
34
59
 
35
60
  class EngineType(Enum):
36
61
  """The type of engine."""
@@ -59,12 +84,12 @@ class _Binding:
59
84
  def name(self) -> str:
60
85
  raise NotImplementedError()
61
86
 
62
- def __enter__(self) -> sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]:
87
+ def __enter__(self) -> scoped_session:
63
88
  raise NotImplementedError()
64
89
 
65
90
  def __exit__(
66
91
  self,
67
- exc_type: Optional[Type[BaseException]],
92
+ exc_type: Optional[type[BaseException]],
68
93
  exc_value: Optional[BaseException],
69
94
  exc_traceback: Optional[TracebackType],
70
95
  ) -> Literal[False]:
@@ -79,14 +104,12 @@ class _NewBinding(_Binding):
79
104
  def name(self) -> str:
80
105
  return self.session.engine_name(self.readwrite)
81
106
 
82
- def __enter__(self) -> sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]:
107
+ def __enter__(self) -> scoped_session:
83
108
  return self.session(None, self.readwrite)
84
109
 
85
110
 
86
111
  class _OldBinding(_Binding):
87
- def __init__(
88
- self, session: sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session], engine: sqlalchemy.engine.Engine
89
- ):
112
+ def __init__(self, session: scoped_session, engine: sqlalchemy.engine.Engine):
90
113
  self.session = session
91
114
  self.engine = engine
92
115
  self.prev_bind = None
@@ -94,14 +117,14 @@ class _OldBinding(_Binding):
94
117
  def name(self) -> str:
95
118
  return cast(str, self.engine.c2c_name) # type: ignore
96
119
 
97
- def __enter__(self) -> sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]:
120
+ def __enter__(self) -> scoped_session:
98
121
  self.prev_bind = self.session.bind # type: ignore
99
122
  self.session.bind = self.engine
100
123
  return self.session
101
124
 
102
125
  def __exit__(
103
126
  self,
104
- exc_type: Optional[Type[BaseException]],
127
+ exc_type: Optional[type[BaseException]],
105
128
  exc_value: Optional[BaseException],
106
129
  exc_traceback: Optional[TracebackType],
107
130
  ) -> Literal[False]:
@@ -110,7 +133,7 @@ class _OldBinding(_Binding):
110
133
 
111
134
 
112
135
  def _get_binding_class(
113
- session: Union[sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session], c2cwsgiutils.db.SessionFactory],
136
+ session: Union[scoped_session, c2cwsgiutils.db.SessionFactory],
114
137
  ro_engin: sqlalchemy.engine.Engine,
115
138
  rw_engin: sqlalchemy.engine.Engine,
116
139
  readwrite: bool,
@@ -122,9 +145,9 @@ def _get_binding_class(
122
145
 
123
146
 
124
147
  def _get_bindings(
125
- session: Union[sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session], c2cwsgiutils.db.SessionFactory],
148
+ session: Union[scoped_session, c2cwsgiutils.db.SessionFactory],
126
149
  engine_type: EngineType,
127
- ) -> List[_Binding]:
150
+ ) -> list[_Binding]:
128
151
  if isinstance(session, c2cwsgiutils.db.SessionFactory):
129
152
  ro_engin = session.ro_engine
130
153
  rw_engin = session.rw_engine
@@ -180,7 +203,7 @@ class HealthCheck:
180
203
  "c2c_health_check", config_utils.get_base_path(config) + r"/health_check", request_method="GET"
181
204
  )
182
205
  config.add_view(self._view, route_name="c2c_health_check", renderer="fast_json", http_cache=0)
183
- self._checks: List[Tuple[str, Callable[[pyramid.request.Request], Any], int]] = []
206
+ self._checks: list[tuple[str, Callable[[pyramid.request.Request], Any], int]] = []
184
207
 
185
208
  self.name = config_utils.env_or_config(
186
209
  config,
@@ -195,8 +218,8 @@ class HealthCheck:
195
218
 
196
219
  def add_db_session_check(
197
220
  self,
198
- session: Union[sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session], c2cwsgiutils.db.SessionFactory],
199
- query_cb: Optional[Callable[[sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]], Any]] = None,
221
+ session: Union[scoped_session, c2cwsgiutils.db.SessionFactory],
222
+ query_cb: Optional[Callable[[scoped_session], Any]] = None,
200
223
  at_least_one_model: Optional[object] = None,
201
224
  level: int = 1,
202
225
  engine_type: EngineType = EngineType.READ_AND_WRITE,
@@ -223,7 +246,7 @@ class HealthCheck:
223
246
 
224
247
  def add_alembic_check(
225
248
  self,
226
- session: sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session],
249
+ session: scoped_session,
227
250
  alembic_ini_path: str,
228
251
  level: int = 2,
229
252
  name: str = "alembic",
@@ -259,7 +282,7 @@ class HealthCheck:
259
282
  assert version_table
260
283
 
261
284
  class _Check:
262
- def __init__(self, session: sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]) -> None:
285
+ def __init__(self, session: scoped_session) -> None:
263
286
  self.session = session
264
287
 
265
288
  def __call__(self, request: pyramid.request.Request) -> str:
@@ -267,20 +290,9 @@ class HealthCheck:
267
290
  assert version_table
268
291
  for binding in _get_bindings(self.session, EngineType.READ_AND_WRITE):
269
292
  with binding as binded_session:
270
- if stats.USE_TAGS:
271
- key = ["sql", "manual", "health_check", "alembic"]
272
- tags: Optional[Dict[str, str]] = {"conf": alembic_ini_path, "con": binding.name()}
273
- else:
274
- key = [
275
- "sql",
276
- "manual",
277
- "health_check",
278
- "alembic",
279
- alembic_ini_path,
280
- binding.name(),
281
- ]
282
- tags = None
283
- with stats.timer_context(key, tags):
293
+ with _PROMETHEUS_DB_SUMMARY.labels(
294
+ configuration=alembic_ini_path, connection=binding.name(), check="alembic"
295
+ ).time():
284
296
  result = binded_session.execute(
285
297
  sqlalchemy.text(
286
298
  "SELECT version_num FROM " # nosec
@@ -290,12 +302,9 @@ class HealthCheck:
290
302
  ).fetchone()
291
303
  assert result is not None
292
304
  (actual_version,) = result
293
- if stats.USE_TAGS:
294
- stats.increment_counter(
295
- ["alembic_version"], 1, tags={"version": actual_version, "name": name}
296
- )
297
- else:
298
- stats.increment_counter(["alembic_version", name, actual_version], 1)
305
+ _PROMETHEUS_ALEMBIC_VERSION.labels(
306
+ version=actual_version, name=name, configuration=alembic_ini_path
307
+ ).set(1)
299
308
  if actual_version != version_:
300
309
  raise Exception( # pylint: disable=broad-exception-raised
301
310
  f"Invalid alembic version (db: {actual_version}, code: {version_})"
@@ -411,19 +420,12 @@ class HealthCheck:
411
420
  level: the level of the health check
412
421
  """
413
422
 
414
- def check(request: pyramid.request.Request) -> Dict[str, Any]:
423
+ def check(request: pyramid.request.Request) -> dict[str, Any]:
424
+ ref = version.get_version()
415
425
  all_versions = _get_all_versions()
416
426
  assert all_versions
417
427
  versions = [e for e in all_versions if e is not None]
418
- # Output the versions we see on the monitoring
419
- v: Optional[str]
420
- for v, count in Counter(versions).items():
421
- if stats.USE_TAGS:
422
- stats.increment_counter(["version"], count, tags={"version": v})
423
- else:
424
- stats.increment_counter(["version", v], count)
425
-
426
- ref = versions[0]
428
+
427
429
  assert all(v == ref for v in versions), "Non identical versions: " + ", ".join(versions)
428
430
  return {"version": ref, "count": len(versions)}
429
431
 
@@ -451,7 +453,7 @@ class HealthCheck:
451
453
  def _view(self, request: pyramid.request.Request) -> Mapping[str, Any]:
452
454
  max_level = int(request.params.get("max_level", "1"))
453
455
  is_auth = auth.is_auth(request)
454
- results: Dict[str, Dict[str, Any]] = {
456
+ results: dict[str, dict[str, Any]] = {
455
457
  "failures": {},
456
458
  "successes": {},
457
459
  }
@@ -475,25 +477,19 @@ class HealthCheck:
475
477
  level: int,
476
478
  name: str,
477
479
  request: pyramid.request.Request,
478
- results: Dict[str, Dict[str, Any]],
480
+ results: dict[str, dict[str, Any]],
479
481
  ) -> None:
480
- start = time.monotonic()
482
+ start = time.perf_counter()
481
483
  try:
482
484
  result = check(request)
483
- results["successes"][name] = {"timing": time.monotonic() - start, "level": level}
485
+ results["successes"][name] = {"timing": time.perf_counter() - start, "level": level}
484
486
  if result is not None:
485
487
  results["successes"][name]["result"] = result
486
- if stats.USE_TAGS:
487
- stats.increment_counter(["health_check"], 1, tags={"name": name, "outcome": "success"})
488
- else:
489
- stats.increment_counter(["health_check", name, "success"], 1)
488
+ _set_success(check_name=name)
490
489
  except Exception as e: # pylint: disable=broad-except
491
- if stats.USE_TAGS:
492
- stats.increment_counter(["health_check"], 1, tags={"name": name, "outcome": "failure"})
493
- else:
494
- stats.increment_counter(["health_check", name, "failure"], 1)
490
+ _PROMETHEUS_HEALTH_CHECKS_FAILURE.labels(name=name).set(1)
495
491
  LOG.warning("Health check %s failed", name, exc_info=True)
496
- failure = {"message": str(e), "timing": time.monotonic() - start, "level": level}
492
+ failure = {"message": str(e), "timing": time.perf_counter() - start, "level": level}
497
493
  if isinstance(e, JsonCheckException) and e.json_data() is not None:
498
494
  failure["result"] = e.json_data()
499
495
  if is_auth or os.environ.get("DEVELOPMENT", "0") != "0":
@@ -503,24 +499,20 @@ class HealthCheck:
503
499
  @staticmethod
504
500
  def _create_db_engine_check(
505
501
  binding: _Binding,
506
- query_cb: Callable[[sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]], None],
507
- ) -> Tuple[str, Callable[[pyramid.request.Request], None]]:
502
+ query_cb: Callable[[scoped_session], None],
503
+ ) -> tuple[str, Callable[[pyramid.request.Request], None]]:
508
504
  def check(request: pyramid.request.Request) -> None:
509
505
  with binding as session:
510
- if stats.USE_TAGS:
511
- key = ["sql", "manual", "health_check", "db"]
512
- tags: Optional[Dict[str, str]] = {"con": binding.name()}
513
- else:
514
- key = ["sql", "manual", "health_check", "db", binding.name()]
515
- tags = None
516
- with stats.timer_context(key, tags):
506
+ with _PROMETHEUS_DB_SUMMARY.labels(
507
+ connection=binding.name(), check="database", configuration="<default>"
508
+ ).time():
517
509
  return query_cb(session)
518
510
 
519
511
  return "db_engine_" + binding.name(), check
520
512
 
521
513
  @staticmethod
522
- def _at_least_one(model: Any) -> Callable[[sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]], Any]:
523
- def query(session: sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]) -> None:
514
+ def _at_least_one(model: Any) -> Callable[[scoped_session], Any]:
515
+ def query(session: scoped_session) -> None:
524
516
  result = session.query(model).first()
525
517
  if result is None:
526
518
  raise HTTPNotFound(model.__name__ + " record not found")
@@ -532,6 +524,13 @@ def _maybe_function(what: Any, request: pyramid.request.Request) -> Any:
532
524
  return what(request) if callable(what) else what
533
525
 
534
526
 
527
+ @broadcast.decorator(expect_answers=False)
528
+ def _set_success(check_name: str) -> None:
529
+ """Set check in success in all process."""
530
+
531
+ _PROMETHEUS_HEALTH_CHECKS_FAILURE.labels(name=check_name).set(0)
532
+
533
+
535
534
  @broadcast.decorator(expect_answers=True)
536
535
  def _get_all_versions() -> Optional[str]:
537
536
  return version.get_version()