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.
- c2cwsgiutils/__init__.py +13 -13
- c2cwsgiutils/acceptance/connection.py +5 -2
- c2cwsgiutils/acceptance/image.py +98 -4
- c2cwsgiutils/acceptance/package-lock.json +1933 -0
- c2cwsgiutils/acceptance/package.json +7 -0
- c2cwsgiutils/acceptance/print.py +4 -4
- c2cwsgiutils/acceptance/screenshot.js +62 -0
- c2cwsgiutils/acceptance/utils.py +14 -22
- c2cwsgiutils/auth.py +4 -4
- c2cwsgiutils/broadcast/__init__.py +15 -7
- c2cwsgiutils/broadcast/interface.py +3 -2
- c2cwsgiutils/broadcast/local.py +3 -2
- c2cwsgiutils/broadcast/redis.py +8 -7
- c2cwsgiutils/client_info.py +5 -5
- c2cwsgiutils/config_utils.py +2 -1
- c2cwsgiutils/coverage_setup.py +2 -2
- c2cwsgiutils/db.py +58 -37
- c2cwsgiutils/db_maintenance_view.py +2 -1
- c2cwsgiutils/debug/_listeners.py +10 -9
- c2cwsgiutils/debug/_views.py +12 -11
- c2cwsgiutils/debug/utils.py +5 -5
- c2cwsgiutils/errors.py +7 -6
- c2cwsgiutils/health_check.py +96 -85
- c2cwsgiutils/index.py +90 -105
- c2cwsgiutils/loader.py +3 -3
- c2cwsgiutils/logging_view.py +3 -2
- c2cwsgiutils/models_graph.py +8 -6
- c2cwsgiutils/prometheus.py +175 -57
- c2cwsgiutils/pyramid.py +4 -2
- c2cwsgiutils/pyramid_logging.py +2 -1
- c2cwsgiutils/redis_stats.py +13 -11
- c2cwsgiutils/redis_utils.py +15 -14
- c2cwsgiutils/request_tracking/__init__.py +36 -30
- c2cwsgiutils/request_tracking/_sql.py +3 -1
- c2cwsgiutils/scripts/genversion.py +4 -4
- c2cwsgiutils/scripts/stats_db.py +130 -68
- c2cwsgiutils/scripts/test_print.py +1 -1
- c2cwsgiutils/sentry.py +2 -1
- c2cwsgiutils/setup_process.py +13 -17
- c2cwsgiutils/sql_profiler/_impl.py +12 -5
- c2cwsgiutils/sqlalchemylogger/README.md +48 -0
- c2cwsgiutils/sqlalchemylogger/_models.py +7 -4
- c2cwsgiutils/sqlalchemylogger/examples/example.py +15 -0
- c2cwsgiutils/sqlalchemylogger/handlers.py +11 -8
- c2cwsgiutils/static/favicon-16x16.png +0 -0
- c2cwsgiutils/static/favicon-32x32.png +0 -0
- c2cwsgiutils/stats_pyramid/__init__.py +7 -11
- c2cwsgiutils/stats_pyramid/_db_spy.py +14 -11
- c2cwsgiutils/stats_pyramid/_pyramid_spy.py +29 -20
- c2cwsgiutils/templates/index.html.mako +50 -0
- c2cwsgiutils/version.py +49 -16
- c2cwsgiutils-5.2.1.dev197.dist-info/LICENSE +22 -0
- {c2cwsgiutils-5.1.7.dev20230901073305.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/METADATA +187 -135
- c2cwsgiutils-5.2.1.dev197.dist-info/RECORD +67 -0
- {c2cwsgiutils-5.1.7.dev20230901073305.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/WHEEL +1 -2
- c2cwsgiutils-5.2.1.dev197.dist-info/entry_points.txt +21 -0
- c2cwsgiutils/acceptance/composition.py +0 -129
- c2cwsgiutils/metrics.py +0 -110
- c2cwsgiutils/scripts/check_es.py +0 -130
- c2cwsgiutils/scripts/coverage_report.py +0 -36
- c2cwsgiutils/stats.py +0 -355
- c2cwsgiutils/stats_pyramid/_views.py +0 -16
- c2cwsgiutils-5.1.7.dev20230901073305.data/scripts/c2cwsgiutils-run +0 -32
- c2cwsgiutils-5.1.7.dev20230901073305.dist-info/LICENSE.txt +0 -28
- c2cwsgiutils-5.1.7.dev20230901073305.dist-info/RECORD +0 -69
- c2cwsgiutils-5.1.7.dev20230901073305.dist-info/entry_points.txt +0 -25
- c2cwsgiutils-5.1.7.dev20230901073305.dist-info/top_level.txt +0 -2
- tests/acceptance/__init__.py +0 -0
- tests/acceptance/test_utils.py +0 -13
c2cwsgiutils/health_check.py
CHANGED
@@ -12,24 +12,50 @@ import re
|
|
12
12
|
import subprocess # nosec
|
13
13
|
import time
|
14
14
|
import traceback
|
15
|
-
from collections import
|
15
|
+
from collections.abc import Mapping
|
16
16
|
from enum import Enum
|
17
17
|
from types import TracebackType
|
18
|
-
from typing import Any, Callable,
|
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,
|
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) ->
|
87
|
+
def __enter__(self) -> scoped_session:
|
62
88
|
raise NotImplementedError()
|
63
89
|
|
64
90
|
def __exit__(
|
65
91
|
self,
|
66
|
-
exc_type: Optional[
|
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) ->
|
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:
|
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) ->
|
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[
|
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[
|
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[
|
148
|
+
session: Union[scoped_session, c2cwsgiutils.db.SessionFactory],
|
123
149
|
engine_type: EngineType,
|
124
|
-
) ->
|
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(
|
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:
|
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[
|
194
|
-
query_cb: Optional[Callable[[
|
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:
|
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:
|
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
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
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
|
-
|
285
|
-
|
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) ->
|
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
|
-
|
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
|
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:
|
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:
|
480
|
+
results: dict[str, dict[str, Any]],
|
467
481
|
) -> None:
|
468
|
-
start = time.
|
482
|
+
start = time.perf_counter()
|
469
483
|
try:
|
470
484
|
result = check(request)
|
471
|
-
results["successes"][name] = {"timing": time.
|
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
|
-
|
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
|
-
|
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.
|
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[[
|
495
|
-
) ->
|
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
|
-
|
499
|
-
|
500
|
-
|
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[[
|
511
|
-
def query(session:
|
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()
|