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.
- c2cwsgiutils/__init__.py +12 -12
- c2cwsgiutils/acceptance/connection.py +5 -2
- c2cwsgiutils/acceptance/image.py +95 -3
- c2cwsgiutils/acceptance/package-lock.json +1933 -0
- c2cwsgiutils/acceptance/package.json +7 -0
- c2cwsgiutils/acceptance/print.py +3 -3
- 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 +6 -5
- c2cwsgiutils/client_info.py +5 -5
- c2cwsgiutils/config_utils.py +2 -1
- c2cwsgiutils/db.py +20 -11
- c2cwsgiutils/db_maintenance_view.py +2 -1
- c2cwsgiutils/debug/_listeners.py +7 -6
- c2cwsgiutils/debug/_views.py +11 -10
- c2cwsgiutils/debug/utils.py +5 -5
- c2cwsgiutils/health_check.py +72 -73
- c2cwsgiutils/index.py +90 -105
- c2cwsgiutils/loader.py +3 -3
- c2cwsgiutils/logging_view.py +3 -2
- c2cwsgiutils/models_graph.py +4 -4
- 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 +11 -5
- c2cwsgiutils/request_tracking/__init__.py +36 -30
- c2cwsgiutils/scripts/genversion.py +4 -4
- c2cwsgiutils/scripts/stats_db.py +92 -60
- c2cwsgiutils/sentry.py +2 -1
- c2cwsgiutils/setup_process.py +12 -16
- c2cwsgiutils/sql_profiler/_impl.py +3 -2
- c2cwsgiutils/sqlalchemylogger/_models.py +2 -2
- c2cwsgiutils/sqlalchemylogger/handlers.py +6 -6
- 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 +27 -21
- c2cwsgiutils/templates/index.html.mako +50 -0
- c2cwsgiutils/version.py +49 -16
- {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/METADATA +168 -99
- c2cwsgiutils-5.2.1.dev197.dist-info/RECORD +67 -0
- {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/WHEEL +1 -1
- c2cwsgiutils/acceptance/composition.py +0 -129
- c2cwsgiutils/metrics.py +0 -110
- c2cwsgiutils/scripts/check_es.py +0 -130
- c2cwsgiutils/stats.py +0 -344
- c2cwsgiutils/stats_pyramid/_views.py +0 -16
- c2cwsgiutils-5.2.1.dist-info/RECORD +0 -66
- {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/LICENSE +0 -0
- {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/entry_points.txt +0 -0
c2cwsgiutils/health_check.py
CHANGED
@@ -12,11 +12,12 @@ 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
|
@@ -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,
|
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) ->
|
87
|
+
def __enter__(self) -> scoped_session:
|
63
88
|
raise NotImplementedError()
|
64
89
|
|
65
90
|
def __exit__(
|
66
91
|
self,
|
67
|
-
exc_type: Optional[
|
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) ->
|
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) ->
|
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[
|
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[
|
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[
|
148
|
+
session: Union[scoped_session, c2cwsgiutils.db.SessionFactory],
|
126
149
|
engine_type: EngineType,
|
127
|
-
) ->
|
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:
|
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[
|
199
|
-
query_cb: Optional[Callable[[
|
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:
|
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:
|
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
|
-
|
271
|
-
|
272
|
-
|
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
|
-
|
294
|
-
|
295
|
-
|
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) ->
|
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
|
-
|
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:
|
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:
|
480
|
+
results: dict[str, dict[str, Any]],
|
479
481
|
) -> None:
|
480
|
-
start = time.
|
482
|
+
start = time.perf_counter()
|
481
483
|
try:
|
482
484
|
result = check(request)
|
483
|
-
results["successes"][name] = {"timing": time.
|
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
|
-
|
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
|
-
|
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.
|
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[[
|
507
|
-
) ->
|
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
|
-
|
511
|
-
|
512
|
-
|
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[[
|
523
|
-
def query(session:
|
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()
|