c2cwsgiutils 5.1.7.dev20230901073305__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 (69) hide show
  1. c2cwsgiutils/__init__.py +13 -13
  2. c2cwsgiutils/acceptance/connection.py +5 -2
  3. c2cwsgiutils/acceptance/image.py +98 -4
  4. c2cwsgiutils/acceptance/package-lock.json +1933 -0
  5. c2cwsgiutils/acceptance/package.json +7 -0
  6. c2cwsgiutils/acceptance/print.py +4 -4
  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 +8 -7
  14. c2cwsgiutils/client_info.py +5 -5
  15. c2cwsgiutils/config_utils.py +2 -1
  16. c2cwsgiutils/coverage_setup.py +2 -2
  17. c2cwsgiutils/db.py +58 -37
  18. c2cwsgiutils/db_maintenance_view.py +2 -1
  19. c2cwsgiutils/debug/_listeners.py +10 -9
  20. c2cwsgiutils/debug/_views.py +12 -11
  21. c2cwsgiutils/debug/utils.py +5 -5
  22. c2cwsgiutils/errors.py +7 -6
  23. c2cwsgiutils/health_check.py +96 -85
  24. c2cwsgiutils/index.py +90 -105
  25. c2cwsgiutils/loader.py +3 -3
  26. c2cwsgiutils/logging_view.py +3 -2
  27. c2cwsgiutils/models_graph.py +8 -6
  28. c2cwsgiutils/prometheus.py +175 -57
  29. c2cwsgiutils/pyramid.py +4 -2
  30. c2cwsgiutils/pyramid_logging.py +2 -1
  31. c2cwsgiutils/redis_stats.py +13 -11
  32. c2cwsgiutils/redis_utils.py +15 -14
  33. c2cwsgiutils/request_tracking/__init__.py +36 -30
  34. c2cwsgiutils/request_tracking/_sql.py +3 -1
  35. c2cwsgiutils/scripts/genversion.py +4 -4
  36. c2cwsgiutils/scripts/stats_db.py +130 -68
  37. c2cwsgiutils/scripts/test_print.py +1 -1
  38. c2cwsgiutils/sentry.py +2 -1
  39. c2cwsgiutils/setup_process.py +13 -17
  40. c2cwsgiutils/sql_profiler/_impl.py +12 -5
  41. c2cwsgiutils/sqlalchemylogger/README.md +48 -0
  42. c2cwsgiutils/sqlalchemylogger/_models.py +7 -4
  43. c2cwsgiutils/sqlalchemylogger/examples/example.py +15 -0
  44. c2cwsgiutils/sqlalchemylogger/handlers.py +11 -8
  45. c2cwsgiutils/static/favicon-16x16.png +0 -0
  46. c2cwsgiutils/static/favicon-32x32.png +0 -0
  47. c2cwsgiutils/stats_pyramid/__init__.py +7 -11
  48. c2cwsgiutils/stats_pyramid/_db_spy.py +14 -11
  49. c2cwsgiutils/stats_pyramid/_pyramid_spy.py +29 -20
  50. c2cwsgiutils/templates/index.html.mako +50 -0
  51. c2cwsgiutils/version.py +49 -16
  52. c2cwsgiutils-5.2.1.dev197.dist-info/LICENSE +22 -0
  53. {c2cwsgiutils-5.1.7.dev20230901073305.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/METADATA +187 -135
  54. c2cwsgiutils-5.2.1.dev197.dist-info/RECORD +67 -0
  55. {c2cwsgiutils-5.1.7.dev20230901073305.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/WHEEL +1 -2
  56. c2cwsgiutils-5.2.1.dev197.dist-info/entry_points.txt +21 -0
  57. c2cwsgiutils/acceptance/composition.py +0 -129
  58. c2cwsgiutils/metrics.py +0 -110
  59. c2cwsgiutils/scripts/check_es.py +0 -130
  60. c2cwsgiutils/scripts/coverage_report.py +0 -36
  61. c2cwsgiutils/stats.py +0 -355
  62. c2cwsgiutils/stats_pyramid/_views.py +0 -16
  63. c2cwsgiutils-5.1.7.dev20230901073305.data/scripts/c2cwsgiutils-run +0 -32
  64. c2cwsgiutils-5.1.7.dev20230901073305.dist-info/LICENSE.txt +0 -28
  65. c2cwsgiutils-5.1.7.dev20230901073305.dist-info/RECORD +0 -69
  66. c2cwsgiutils-5.1.7.dev20230901073305.dist-info/entry_points.txt +0 -25
  67. c2cwsgiutils-5.1.7.dev20230901073305.dist-info/top_level.txt +0 -2
  68. tests/acceptance/__init__.py +0 -0
  69. tests/acceptance/test_utils.py +0 -13
@@ -12,24 +12,50 @@ 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
23
24
  import sqlalchemy.engine
24
25
  import sqlalchemy.orm
26
+ import sqlalchemy.sql
25
27
  from pyramid.httpexceptions import HTTPNotFound
26
28
 
27
29
  import c2cwsgiutils.db
28
- 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
29
36
 
30
37
  LOG = logging.getLogger(__name__)
31
38
  ALEMBIC_HEAD_RE = re.compile(r"^([a-f0-9]+) \(head\)\n$")
32
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
+
33
59
 
34
60
  class EngineType(Enum):
35
61
  """The type of engine."""
@@ -58,12 +84,12 @@ class _Binding:
58
84
  def name(self) -> str:
59
85
  raise NotImplementedError()
60
86
 
61
- def __enter__(self) -> sqlalchemy.orm.Session:
87
+ def __enter__(self) -> scoped_session:
62
88
  raise NotImplementedError()
63
89
 
64
90
  def __exit__(
65
91
  self,
66
- exc_type: Optional[Type[BaseException]],
92
+ exc_type: Optional[type[BaseException]],
67
93
  exc_value: Optional[BaseException],
68
94
  exc_traceback: Optional[TracebackType],
69
95
  ) -> Literal[False]:
@@ -78,27 +104,27 @@ class _NewBinding(_Binding):
78
104
  def name(self) -> str:
79
105
  return self.session.engine_name(self.readwrite)
80
106
 
81
- def __enter__(self) -> sqlalchemy.orm.Session:
107
+ def __enter__(self) -> scoped_session:
82
108
  return self.session(None, self.readwrite)
83
109
 
84
110
 
85
111
  class _OldBinding(_Binding):
86
- def __init__(self, session: sqlalchemy.orm.scoping.scoped_session, engine: sqlalchemy.engine.Engine):
112
+ def __init__(self, session: scoped_session, engine: sqlalchemy.engine.Engine):
87
113
  self.session = session
88
114
  self.engine = engine
89
115
  self.prev_bind = None
90
116
 
91
117
  def name(self) -> str:
92
- return cast(str, self.engine.c2c_name)
118
+ return cast(str, self.engine.c2c_name) # type: ignore
93
119
 
94
- def __enter__(self) -> sqlalchemy.orm.Session:
95
- self.prev_bind = self.session.bind
120
+ def __enter__(self) -> scoped_session:
121
+ self.prev_bind = self.session.bind # type: ignore
96
122
  self.session.bind = self.engine
97
123
  return self.session
98
124
 
99
125
  def __exit__(
100
126
  self,
101
- exc_type: Optional[Type[BaseException]],
127
+ exc_type: Optional[type[BaseException]],
102
128
  exc_value: Optional[BaseException],
103
129
  exc_traceback: Optional[TracebackType],
104
130
  ) -> Literal[False]:
@@ -107,7 +133,7 @@ class _OldBinding(_Binding):
107
133
 
108
134
 
109
135
  def _get_binding_class(
110
- session: Union[sqlalchemy.orm.scoping.scoped_session, c2cwsgiutils.db.SessionFactory],
136
+ session: Union[scoped_session, c2cwsgiutils.db.SessionFactory],
111
137
  ro_engin: sqlalchemy.engine.Engine,
112
138
  rw_engin: sqlalchemy.engine.Engine,
113
139
  readwrite: bool,
@@ -119,15 +145,15 @@ def _get_binding_class(
119
145
 
120
146
 
121
147
  def _get_bindings(
122
- session: Union[sqlalchemy.orm.scoping.scoped_session, c2cwsgiutils.db.SessionFactory],
148
+ session: Union[scoped_session, c2cwsgiutils.db.SessionFactory],
123
149
  engine_type: EngineType,
124
- ) -> List[sqlalchemy.engine.Engine]:
150
+ ) -> list[_Binding]:
125
151
  if isinstance(session, c2cwsgiutils.db.SessionFactory):
126
152
  ro_engin = session.ro_engine
127
153
  rw_engin = session.rw_engine
128
154
  else:
129
- ro_engin = session.c2c_ro_bind
130
- rw_engin = session.c2c_rw_bind
155
+ ro_engin = session.c2c_ro_bind # type: ignore
156
+ rw_engin = session.c2c_rw_bind # type: ignore
131
157
 
132
158
  if rw_engin == ro_engin:
133
159
  engine_type = EngineType.WRITE_ONLY
@@ -159,7 +185,9 @@ def _get_alembic_version(alembic_ini_path: str, name: str) -> str:
159
185
  ).decode("utf-8")
160
186
  out_match = ALEMBIC_HEAD_RE.match(out)
161
187
  if not out_match:
162
- raise Exception("Cannot get the alembic HEAD version from: " + out)
188
+ raise Exception( # pylint: disable=broad-exception-raised
189
+ "Cannot get the alembic HEAD version from: " + out
190
+ )
163
191
  return out_match.group(1)
164
192
 
165
193
 
@@ -175,7 +203,7 @@ class HealthCheck:
175
203
  "c2c_health_check", config_utils.get_base_path(config) + r"/health_check", request_method="GET"
176
204
  )
177
205
  config.add_view(self._view, route_name="c2c_health_check", renderer="fast_json", http_cache=0)
178
- self._checks: List[Tuple[str, Callable[[pyramid.request.Request], Any], int]] = []
206
+ self._checks: list[tuple[str, Callable[[pyramid.request.Request], Any], int]] = []
179
207
 
180
208
  self.name = config_utils.env_or_config(
181
209
  config,
@@ -190,8 +218,8 @@ class HealthCheck:
190
218
 
191
219
  def add_db_session_check(
192
220
  self,
193
- session: Union[sqlalchemy.orm.scoping.scoped_session, c2cwsgiutils.db.SessionFactory],
194
- query_cb: Optional[Callable[[sqlalchemy.orm.scoping.scoped_session], Any]] = None,
221
+ session: Union[scoped_session, c2cwsgiutils.db.SessionFactory],
222
+ query_cb: Optional[Callable[[scoped_session], Any]] = None,
195
223
  at_least_one_model: Optional[object] = None,
196
224
  level: int = 1,
197
225
  engine_type: EngineType = EngineType.READ_AND_WRITE,
@@ -218,7 +246,7 @@ class HealthCheck:
218
246
 
219
247
  def add_alembic_check(
220
248
  self,
221
- session: Union[sqlalchemy.orm.scoping.scoped_session, c2cwsgiutils.db.SessionFactory],
249
+ session: scoped_session,
222
250
  alembic_ini_path: str,
223
251
  level: int = 2,
224
252
  name: str = "alembic",
@@ -247,44 +275,38 @@ class HealthCheck:
247
275
 
248
276
  if version_schema is None:
249
277
  version_schema = config.get(name, "version_table_schema", fallback="public")
278
+ assert version_schema
250
279
 
251
280
  if version_table is None:
252
281
  version_table = config.get(name, "version_table", fallback="alembic_version")
282
+ assert version_table
253
283
 
254
284
  class _Check:
255
- def __init__(self, session: sqlalchemy.orm.scoping.scoped_session) -> None:
285
+ def __init__(self, session: scoped_session) -> None:
256
286
  self.session = session
257
287
 
258
288
  def __call__(self, request: pyramid.request.Request) -> str:
289
+ assert version_schema
290
+ assert version_table
259
291
  for binding in _get_bindings(self.session, EngineType.READ_AND_WRITE):
260
- with binding as session:
261
- if stats.USE_TAGS:
262
- key = ["sql", "manual", "health_check", "alembic"]
263
- tags: Optional[Dict[str, str]] = dict(conf=alembic_ini_path, con=binding.name())
264
- else:
265
- key = [
266
- "sql",
267
- "manual",
268
- "health_check",
269
- "alembic",
270
- alembic_ini_path,
271
- binding.name(),
272
- ]
273
- tags = None
274
- with stats.timer_context(key, tags):
275
- quote = session.bind.dialect.identifier_preparer.quote
276
- (actual_version,) = session.execute(
277
- "SELECT version_num FROM " # nosec
278
- f"{quote(version_schema)}.{quote(version_table)}"
279
- ).fetchone()
280
- if stats.USE_TAGS:
281
- stats.increment_counter(
282
- ["alembic_version"], 1, tags=dict(version=actual_version, name=name)
292
+ with binding as binded_session:
293
+ with _PROMETHEUS_DB_SUMMARY.labels(
294
+ configuration=alembic_ini_path, connection=binding.name(), check="alembic"
295
+ ).time():
296
+ result = binded_session.execute(
297
+ sqlalchemy.text(
298
+ "SELECT version_num FROM " # nosec
299
+ f"{sqlalchemy.sql.quoted_name(version_schema, True)}."
300
+ f"{sqlalchemy.sql.quoted_name(version_table, True)}"
283
301
  )
284
- else:
285
- stats.increment_counter(["alembic_version", name, actual_version], 1)
302
+ ).fetchone()
303
+ assert result is not None
304
+ (actual_version,) = result
305
+ _PROMETHEUS_ALEMBIC_VERSION.labels(
306
+ version=actual_version, name=name, configuration=alembic_ini_path
307
+ ).set(1)
286
308
  if actual_version != version_:
287
- raise Exception(
309
+ raise Exception( # pylint: disable=broad-exception-raised
288
310
  f"Invalid alembic version (db: {actual_version}, code: {version_})"
289
311
  )
290
312
  return version_
@@ -392,28 +414,20 @@ class HealthCheck:
392
414
  """
393
415
  Check that the version matches across all instances.
394
416
 
395
- Arguments:
417
+ Keyword Arguments:
396
418
 
397
419
  name: the name of the check (defaults to "version")
398
420
  level: the level of the health check
399
- :return:
400
421
  """
401
422
 
402
- def check(request: pyramid.request.Request) -> Dict[str, Any]:
423
+ def check(request: pyramid.request.Request) -> dict[str, Any]:
424
+ ref = version.get_version()
403
425
  all_versions = _get_all_versions()
404
426
  assert all_versions
405
427
  versions = [e for e in all_versions if e is not None]
406
- # Output the versions we see on the monitoring
407
- v: Optional[str]
408
- for v, count in Counter(versions).items():
409
- if stats.USE_TAGS:
410
- stats.increment_counter(["version"], count, tags=dict(version=v))
411
- else:
412
- stats.increment_counter(["version", v], count)
413
-
414
- ref = versions[0]
428
+
415
429
  assert all(v == ref for v in versions), "Non identical versions: " + ", ".join(versions)
416
- return dict(version=ref, count=len(versions))
430
+ return {"version": ref, "count": len(versions)}
417
431
 
418
432
  assert name
419
433
  self._checks.append((name, check, level))
@@ -439,7 +453,7 @@ class HealthCheck:
439
453
  def _view(self, request: pyramid.request.Request) -> Mapping[str, Any]:
440
454
  max_level = int(request.params.get("max_level", "1"))
441
455
  is_auth = auth.is_auth(request)
442
- results: Dict[str, Dict[str, Any]] = {
456
+ results: dict[str, dict[str, Any]] = {
443
457
  "failures": {},
444
458
  "successes": {},
445
459
  }
@@ -463,25 +477,19 @@ class HealthCheck:
463
477
  level: int,
464
478
  name: str,
465
479
  request: pyramid.request.Request,
466
- results: Dict[str, Dict[str, Any]],
480
+ results: dict[str, dict[str, Any]],
467
481
  ) -> None:
468
- start = time.monotonic()
482
+ start = time.perf_counter()
469
483
  try:
470
484
  result = check(request)
471
- results["successes"][name] = {"timing": time.monotonic() - start, "level": level}
485
+ results["successes"][name] = {"timing": time.perf_counter() - start, "level": level}
472
486
  if result is not None:
473
487
  results["successes"][name]["result"] = result
474
- if stats.USE_TAGS:
475
- stats.increment_counter(["health_check"], 1, tags=dict(name=name, outcome="success"))
476
- else:
477
- stats.increment_counter(["health_check", name, "success"], 1)
488
+ _set_success(check_name=name)
478
489
  except Exception as e: # pylint: disable=broad-except
479
- if stats.USE_TAGS:
480
- stats.increment_counter(["health_check"], 1, tags=dict(name=name, outcome="failure"))
481
- else:
482
- stats.increment_counter(["health_check", name, "failure"], 1)
490
+ _PROMETHEUS_HEALTH_CHECKS_FAILURE.labels(name=name).set(1)
483
491
  LOG.warning("Health check %s failed", name, exc_info=True)
484
- failure = {"message": str(e), "timing": time.monotonic() - start, "level": level}
492
+ failure = {"message": str(e), "timing": time.perf_counter() - start, "level": level}
485
493
  if isinstance(e, JsonCheckException) and e.json_data() is not None:
486
494
  failure["result"] = e.json_data()
487
495
  if is_auth or os.environ.get("DEVELOPMENT", "0") != "0":
@@ -491,24 +499,20 @@ class HealthCheck:
491
499
  @staticmethod
492
500
  def _create_db_engine_check(
493
501
  binding: _Binding,
494
- query_cb: Callable[[sqlalchemy.orm.scoping.scoped_session], None],
495
- ) -> Tuple[str, Callable[[pyramid.request.Request], None]]:
502
+ query_cb: Callable[[scoped_session], None],
503
+ ) -> tuple[str, Callable[[pyramid.request.Request], None]]:
496
504
  def check(request: pyramid.request.Request) -> None:
497
505
  with binding as session:
498
- if stats.USE_TAGS:
499
- key = ["sql", "manual", "health_check", "db"]
500
- tags: Optional[Dict[str, str]] = dict(con=binding.name())
501
- else:
502
- key = ["sql", "manual", "health_check", "db", binding.name()]
503
- tags = None
504
- with stats.timer_context(key, tags):
506
+ with _PROMETHEUS_DB_SUMMARY.labels(
507
+ connection=binding.name(), check="database", configuration="<default>"
508
+ ).time():
505
509
  return query_cb(session)
506
510
 
507
511
  return "db_engine_" + binding.name(), check
508
512
 
509
513
  @staticmethod
510
- def _at_least_one(model: Any) -> Callable[[sqlalchemy.orm.scoping.scoped_session], Any]:
511
- def query(session: sqlalchemy.orm.scoping.scoped_session) -> None:
514
+ def _at_least_one(model: Any) -> Callable[[scoped_session], Any]:
515
+ def query(session: scoped_session) -> None:
512
516
  result = session.query(model).first()
513
517
  if result is None:
514
518
  raise HTTPNotFound(model.__name__ + " record not found")
@@ -520,6 +524,13 @@ def _maybe_function(what: Any, request: pyramid.request.Request) -> Any:
520
524
  return what(request) if callable(what) else what
521
525
 
522
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
+
523
534
  @broadcast.decorator(expect_answers=True)
524
535
  def _get_all_versions() -> Optional[str]:
525
536
  return version.get_version()