c2cwsgiutils 6.0.10.dev9__py3-none-any.whl → 6.1.0__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 +14 -11
- c2cwsgiutils/acceptance/__init__.py +2 -3
- c2cwsgiutils/acceptance/connection.py +1 -2
- c2cwsgiutils/acceptance/image.py +9 -17
- c2cwsgiutils/acceptance/package-lock.json +270 -1062
- c2cwsgiutils/acceptance/package.json +2 -2
- c2cwsgiutils/acceptance/print.py +7 -3
- c2cwsgiutils/acceptance/utils.py +1 -3
- c2cwsgiutils/auth.py +43 -37
- c2cwsgiutils/broadcast/__init__.py +16 -16
- c2cwsgiutils/broadcast/interface.py +3 -3
- c2cwsgiutils/broadcast/local.py +1 -0
- c2cwsgiutils/broadcast/redis.py +13 -12
- c2cwsgiutils/client_info.py +13 -5
- c2cwsgiutils/config_utils.py +1 -0
- c2cwsgiutils/coverage_setup.py +4 -3
- c2cwsgiutils/db.py +36 -41
- c2cwsgiutils/db_maintenance_view.py +13 -13
- c2cwsgiutils/debug/__init__.py +2 -2
- c2cwsgiutils/debug/_listeners.py +1 -1
- c2cwsgiutils/debug/_views.py +7 -6
- c2cwsgiutils/debug/utils.py +9 -9
- c2cwsgiutils/errors.py +13 -14
- c2cwsgiutils/health_check.py +25 -30
- c2cwsgiutils/index.py +14 -16
- c2cwsgiutils/loader.py +1 -1
- c2cwsgiutils/logging_view.py +12 -12
- c2cwsgiutils/models_graph.py +0 -1
- c2cwsgiutils/pretty_json.py +0 -1
- c2cwsgiutils/prometheus.py +1 -7
- c2cwsgiutils/pyramid.py +0 -1
- c2cwsgiutils/pyramid_logging.py +2 -1
- c2cwsgiutils/redis_stats.py +9 -9
- c2cwsgiutils/redis_utils.py +19 -18
- c2cwsgiutils/request_tracking/__init__.py +14 -13
- c2cwsgiutils/request_tracking/_sql.py +0 -1
- c2cwsgiutils/scripts/genversion.py +5 -5
- c2cwsgiutils/scripts/stats_db.py +19 -17
- c2cwsgiutils/scripts/test_print.py +5 -5
- c2cwsgiutils/sentry.py +55 -20
- c2cwsgiutils/services.py +2 -2
- c2cwsgiutils/setup_process.py +0 -2
- c2cwsgiutils/sql_profiler/__init__.py +6 -6
- c2cwsgiutils/sql_profiler/_impl.py +19 -17
- c2cwsgiutils/sqlalchemylogger/README.md +30 -13
- c2cwsgiutils/sqlalchemylogger/handlers.py +12 -11
- c2cwsgiutils/stats_pyramid/__init__.py +1 -5
- c2cwsgiutils/stats_pyramid/_db_spy.py +2 -2
- c2cwsgiutils/stats_pyramid/_pyramid_spy.py +0 -1
- c2cwsgiutils/version.py +11 -5
- {c2cwsgiutils-6.0.10.dev9.dist-info → c2cwsgiutils-6.1.0.dist-info}/LICENSE +1 -1
- {c2cwsgiutils-6.0.10.dev9.dist-info → c2cwsgiutils-6.1.0.dist-info}/METADATA +12 -12
- c2cwsgiutils-6.1.0.dist-info/RECORD +67 -0
- {c2cwsgiutils-6.0.10.dev9.dist-info → c2cwsgiutils-6.1.0.dist-info}/WHEEL +1 -1
- c2cwsgiutils-6.0.10.dev9.dist-info/RECORD +0 -67
- {c2cwsgiutils-6.0.10.dev9.dist-info → c2cwsgiutils-6.1.0.dist-info}/entry_points.txt +0 -0
c2cwsgiutils/health_check.py
CHANGED
@@ -4,6 +4,7 @@ Setup an health_check API.
|
|
4
4
|
To use it, create an instance of this class in your application initialization and do a few calls to its
|
5
5
|
methods add_db_check()
|
6
6
|
"""
|
7
|
+
|
7
8
|
import configparser
|
8
9
|
import copy
|
9
10
|
import logging
|
@@ -15,7 +16,7 @@ import traceback
|
|
15
16
|
from collections.abc import Mapping
|
16
17
|
from enum import Enum
|
17
18
|
from types import TracebackType
|
18
|
-
from typing import
|
19
|
+
from typing import Any, Callable, Literal, Optional, Union, cast
|
19
20
|
|
20
21
|
import prometheus_client
|
21
22
|
import pyramid.config
|
@@ -29,13 +30,10 @@ from pyramid.httpexceptions import HTTPNotFound
|
|
29
30
|
import c2cwsgiutils.db
|
30
31
|
from c2cwsgiutils import auth, broadcast, config_utils, prometheus, redis_utils, version
|
31
32
|
|
32
|
-
|
33
|
-
scoped_session = sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]
|
34
|
-
else:
|
35
|
-
scoped_session = sqlalchemy.orm.scoped_session
|
33
|
+
_scoped_session = sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]
|
36
34
|
|
37
|
-
|
38
|
-
|
35
|
+
_LOG = logging.getLogger(__name__)
|
36
|
+
_ALEMBIC_HEAD_RE = re.compile(r"^([a-f0-9]+) \(head\)\n$")
|
39
37
|
|
40
38
|
_PROMETHEUS_DB_SUMMARY = prometheus_client.Summary(
|
41
39
|
prometheus.build_metric_name("health_check_db"),
|
@@ -77,14 +75,16 @@ class JsonCheckException(Exception):
|
|
77
75
|
return self.message
|
78
76
|
|
79
77
|
def json_data(self) -> Any:
|
78
|
+
"""Return the JSON data to be returned in the response."""
|
80
79
|
return self.json
|
81
80
|
|
82
81
|
|
83
82
|
class _Binding:
|
84
83
|
def name(self) -> str:
|
84
|
+
"""Return the name of the binding."""
|
85
85
|
raise NotImplementedError()
|
86
86
|
|
87
|
-
def __enter__(self) ->
|
87
|
+
def __enter__(self) -> _scoped_session:
|
88
88
|
raise NotImplementedError()
|
89
89
|
|
90
90
|
def __exit__(
|
@@ -104,12 +104,12 @@ class _NewBinding(_Binding):
|
|
104
104
|
def name(self) -> str:
|
105
105
|
return self.session.engine_name(self.readwrite)
|
106
106
|
|
107
|
-
def __enter__(self) ->
|
107
|
+
def __enter__(self) -> _scoped_session:
|
108
108
|
return self.session(None, self.readwrite)
|
109
109
|
|
110
110
|
|
111
111
|
class _OldBinding(_Binding):
|
112
|
-
def __init__(self, session:
|
112
|
+
def __init__(self, session: _scoped_session, engine: sqlalchemy.engine.Engine):
|
113
113
|
self.session = session
|
114
114
|
self.engine = engine
|
115
115
|
self.prev_bind = None
|
@@ -117,7 +117,7 @@ class _OldBinding(_Binding):
|
|
117
117
|
def name(self) -> str:
|
118
118
|
return cast(str, self.engine.c2c_name) # type: ignore
|
119
119
|
|
120
|
-
def __enter__(self) ->
|
120
|
+
def __enter__(self) -> _scoped_session:
|
121
121
|
self.prev_bind = self.session.bind # type: ignore
|
122
122
|
self.session.bind = self.engine
|
123
123
|
return self.session
|
@@ -133,7 +133,7 @@ class _OldBinding(_Binding):
|
|
133
133
|
|
134
134
|
|
135
135
|
def _get_binding_class(
|
136
|
-
session: Union[
|
136
|
+
session: Union[_scoped_session, c2cwsgiutils.db.SessionFactory],
|
137
137
|
ro_engin: sqlalchemy.engine.Engine,
|
138
138
|
rw_engin: sqlalchemy.engine.Engine,
|
139
139
|
readwrite: bool,
|
@@ -145,7 +145,7 @@ def _get_binding_class(
|
|
145
145
|
|
146
146
|
|
147
147
|
def _get_bindings(
|
148
|
-
session: Union[
|
148
|
+
session: Union[_scoped_session, c2cwsgiutils.db.SessionFactory],
|
149
149
|
engine_type: EngineType,
|
150
150
|
) -> list[_Binding]:
|
151
151
|
if isinstance(session, c2cwsgiutils.db.SessionFactory):
|
@@ -183,7 +183,7 @@ def _get_alembic_version(alembic_ini_path: str, name: str) -> str:
|
|
183
183
|
out = subprocess.check_output( # nosec
|
184
184
|
["alembic", "--config", alembic_ini_path, "--name", name, "heads"], cwd=dirname, env=env
|
185
185
|
).decode("utf-8")
|
186
|
-
out_match =
|
186
|
+
out_match = _ALEMBIC_HEAD_RE.match(out)
|
187
187
|
if not out_match:
|
188
188
|
raise Exception( # pylint: disable=broad-exception-raised
|
189
189
|
"Cannot get the alembic HEAD version from: " + out
|
@@ -218,8 +218,8 @@ class HealthCheck:
|
|
218
218
|
|
219
219
|
def add_db_session_check(
|
220
220
|
self,
|
221
|
-
session: Union[
|
222
|
-
query_cb: Optional[Callable[[
|
221
|
+
session: Union[_scoped_session, c2cwsgiutils.db.SessionFactory],
|
222
|
+
query_cb: Optional[Callable[[_scoped_session], Any]] = None,
|
223
223
|
at_least_one_model: Optional[object] = None,
|
224
224
|
level: int = 1,
|
225
225
|
engine_type: EngineType = EngineType.READ_AND_WRITE,
|
@@ -228,7 +228,6 @@ class HealthCheck:
|
|
228
228
|
Check a DB session is working. You can specify either query_cb or at_least_one_model.
|
229
229
|
|
230
230
|
Arguments:
|
231
|
-
|
232
231
|
session: a DB session created by c2cwsgiutils.db.init()
|
233
232
|
query_cb: a callable that take a session as parameter and check it works
|
234
233
|
at_least_one_model: a model that must have at least one entry in the DB
|
@@ -246,7 +245,7 @@ class HealthCheck:
|
|
246
245
|
|
247
246
|
def add_alembic_check(
|
248
247
|
self,
|
249
|
-
session:
|
248
|
+
session: _scoped_session,
|
250
249
|
alembic_ini_path: str,
|
251
250
|
level: int = 2,
|
252
251
|
name: str = "alembic",
|
@@ -257,7 +256,6 @@ class HealthCheck:
|
|
257
256
|
Check the DB version against the HEAD version of Alembic.
|
258
257
|
|
259
258
|
Arguments:
|
260
|
-
|
261
259
|
session: A DB session created by c2cwsgiutils.db.init() giving access to the DB \
|
262
260
|
managed by Alembic
|
263
261
|
alembic_ini_path: Path to the Alembic INI file
|
@@ -282,7 +280,7 @@ class HealthCheck:
|
|
282
280
|
assert version_table
|
283
281
|
|
284
282
|
class _Check:
|
285
|
-
def __init__(self, session:
|
283
|
+
def __init__(self, session: _scoped_session) -> None:
|
286
284
|
self.session = session
|
287
285
|
|
288
286
|
def __call__(self, request: pyramid.request.Request) -> str:
|
@@ -333,7 +331,6 @@ class HealthCheck:
|
|
333
331
|
Check that a GET on an URL returns 2xx.
|
334
332
|
|
335
333
|
Arguments:
|
336
|
-
|
337
334
|
url: the URL to query or a function taking the request and returning it
|
338
335
|
params: the parameters or a function taking the request and returning them
|
339
336
|
headers: the headers or a function taking the request and returning them
|
@@ -366,7 +363,6 @@ class HealthCheck:
|
|
366
363
|
One such check is automatically added if the broadcaster is configured with redis.
|
367
364
|
|
368
365
|
Arguments:
|
369
|
-
|
370
366
|
name: the name of the check (defaults to url)
|
371
367
|
level: the level of the health check
|
372
368
|
"""
|
@@ -414,13 +410,13 @@ class HealthCheck:
|
|
414
410
|
"""
|
415
411
|
Check that the version matches across all instances.
|
416
412
|
|
417
|
-
|
418
|
-
|
413
|
+
Arguments:
|
419
414
|
name: the name of the check (defaults to "version")
|
420
415
|
level: the level of the health check
|
421
416
|
"""
|
422
417
|
|
423
418
|
def check(request: pyramid.request.Request) -> dict[str, Any]:
|
419
|
+
del request # unused
|
424
420
|
ref = version.get_version()
|
425
421
|
all_versions = _get_all_versions()
|
426
422
|
assert all_versions
|
@@ -442,7 +438,6 @@ class HealthCheck:
|
|
442
438
|
in the response. In case of failure it must raise an exception.
|
443
439
|
|
444
440
|
Arguments:
|
445
|
-
|
446
441
|
name: the name of the check
|
447
442
|
check_cb: the callback to call (takes the request as parameter)
|
448
443
|
level: the level of the health check
|
@@ -488,7 +483,7 @@ class HealthCheck:
|
|
488
483
|
_set_success(check_name=name)
|
489
484
|
except Exception as e: # pylint: disable=broad-except
|
490
485
|
_PROMETHEUS_HEALTH_CHECKS_FAILURE.labels(name=name).set(1)
|
491
|
-
|
486
|
+
_LOG.warning("Health check %s failed", name, exc_info=True)
|
492
487
|
failure = {"message": str(e), "timing": time.perf_counter() - start, "level": level}
|
493
488
|
if isinstance(e, JsonCheckException) and e.json_data() is not None:
|
494
489
|
failure["result"] = e.json_data()
|
@@ -499,9 +494,10 @@ class HealthCheck:
|
|
499
494
|
@staticmethod
|
500
495
|
def _create_db_engine_check(
|
501
496
|
binding: _Binding,
|
502
|
-
query_cb: Callable[[
|
497
|
+
query_cb: Callable[[_scoped_session], None],
|
503
498
|
) -> tuple[str, Callable[[pyramid.request.Request], None]]:
|
504
499
|
def check(request: pyramid.request.Request) -> None:
|
500
|
+
del request # unused
|
505
501
|
with binding as session:
|
506
502
|
with _PROMETHEUS_DB_SUMMARY.labels(
|
507
503
|
connection=binding.name(), check="database", configuration="<default>"
|
@@ -511,8 +507,8 @@ class HealthCheck:
|
|
511
507
|
return "db_engine_" + binding.name(), check
|
512
508
|
|
513
509
|
@staticmethod
|
514
|
-
def _at_least_one(model: Any) -> Callable[[
|
515
|
-
def query(session:
|
510
|
+
def _at_least_one(model: Any) -> Callable[[_scoped_session], Any]:
|
511
|
+
def query(session: _scoped_session) -> None:
|
516
512
|
result = session.query(model).first()
|
517
513
|
if result is None:
|
518
514
|
raise HTTPNotFound(model.__name__ + " record not found")
|
@@ -527,7 +523,6 @@ def _maybe_function(what: Any, request: pyramid.request.Request) -> Any:
|
|
527
523
|
@broadcast.decorator(expect_answers=False)
|
528
524
|
def _set_success(check_name: str) -> None:
|
529
525
|
"""Set check in success in all process."""
|
530
|
-
|
531
526
|
_PROMETHEUS_HEALTH_CHECKS_FAILURE.labels(name=check_name).set(0)
|
532
527
|
|
533
528
|
|
c2cwsgiutils/index.py
CHANGED
@@ -42,12 +42,12 @@ from c2cwsgiutils.auth import (
|
|
42
42
|
)
|
43
43
|
from c2cwsgiutils.config_utils import env_or_settings
|
44
44
|
|
45
|
-
|
45
|
+
_LOG = logging.getLogger(__name__)
|
46
46
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
47
|
+
_additional_title: Optional[str] = None
|
48
|
+
_additional_noauth: list[str] = []
|
49
|
+
_additional_auth: list[str] = []
|
50
|
+
_ELEM_ID = 0
|
51
51
|
|
52
52
|
|
53
53
|
def _url(
|
@@ -113,9 +113,9 @@ def input_(
|
|
113
113
|
name: str, label: Optional[str] = None, type_: Optional[str] = None, value: Union[str, int] = ""
|
114
114
|
) -> str:
|
115
115
|
"""Get an HTML input."""
|
116
|
-
global
|
117
|
-
id_ =
|
118
|
-
|
116
|
+
global _ELEM_ID # pylint: disable=global-statement
|
117
|
+
id_ = _ELEM_ID
|
118
|
+
_ELEM_ID += 1
|
119
119
|
|
120
120
|
if label is None and type_ != "hidden":
|
121
121
|
label = name.replace("_", " ").capitalize()
|
@@ -138,7 +138,6 @@ def input_(
|
|
138
138
|
|
139
139
|
def button(label: str) -> str:
|
140
140
|
"""Get en HTML button."""
|
141
|
-
|
142
141
|
return f'<button class="btn btn-primary" type="submit">{label}</button>'
|
143
142
|
|
144
143
|
|
@@ -153,22 +152,22 @@ def _index(request: pyramid.request.Request) -> dict[str, str]:
|
|
153
152
|
body = ""
|
154
153
|
body += _health_check(request)
|
155
154
|
body += _stats(request)
|
156
|
-
body += _versions(request)
|
157
155
|
if has_access:
|
156
|
+
body += _versions(request)
|
158
157
|
body += _debug(request)
|
159
158
|
body += _db_maintenance(request)
|
160
159
|
body += _logging(request)
|
161
160
|
body += _profiler(request)
|
162
161
|
|
163
|
-
if
|
164
|
-
body +=
|
162
|
+
if _additional_title is not None and (has_access or _additional_noauth):
|
163
|
+
body += _additional_title
|
165
164
|
body += "\n"
|
166
165
|
|
167
166
|
if has_access:
|
168
|
-
body += "\n".join(
|
167
|
+
body += "\n".join(_additional_auth)
|
169
168
|
body += "\n"
|
170
169
|
|
171
|
-
body += "\n".join(
|
170
|
+
body += "\n".join(_additional_noauth)
|
172
171
|
|
173
172
|
settings = request.registry.settings
|
174
173
|
auth_type_ = auth_type(settings)
|
@@ -346,7 +345,6 @@ def _health_check(request: pyramid.request.Request) -> str:
|
|
346
345
|
|
347
346
|
def _github_login(request: pyramid.request.Request) -> dict[str, Any]:
|
348
347
|
"""Get the view that start the authentication on GitHub."""
|
349
|
-
|
350
348
|
settings = request.registry.settings
|
351
349
|
params = dict(request.params)
|
352
350
|
callback_url = _url(
|
@@ -500,7 +498,7 @@ def includeme(config: pyramid.config.Configurator) -> None:
|
|
500
498
|
settings = config.get_settings()
|
501
499
|
auth_type_ = auth_type(settings)
|
502
500
|
if auth_type_ == AuthenticationType.SECRET:
|
503
|
-
|
501
|
+
_LOG.warning(
|
504
502
|
"It is recommended to use OAuth2 with GitHub login instead of the `C2C_SECRET` because it "
|
505
503
|
"protects from brute force attacks and the access grant is personal and can be revoked."
|
506
504
|
)
|
c2cwsgiutils/loader.py
CHANGED
c2cwsgiutils/logging_view.py
CHANGED
@@ -7,10 +7,10 @@ import pyramid.request
|
|
7
7
|
|
8
8
|
from c2cwsgiutils import auth, broadcast, config_utils, redis_utils
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
_LOG = logging.getLogger(__name__)
|
11
|
+
_CONFIG_KEY = "c2c.log_view_enabled"
|
12
|
+
_ENV_KEY = "C2C_LOG_VIEW_ENABLED"
|
13
|
+
_REDIS_PREFIX = "c2c_logging_level_"
|
14
14
|
|
15
15
|
|
16
16
|
def install_subscriber(config: pyramid.config.Configurator) -> None:
|
@@ -21,7 +21,7 @@ def install_subscriber(config: pyramid.config.Configurator) -> None:
|
|
21
21
|
|
22
22
|
def includeme(config: pyramid.config.Configurator) -> None:
|
23
23
|
"""Install the view to configure the loggers, if configured to do so."""
|
24
|
-
if auth.is_enabled(config,
|
24
|
+
if auth.is_enabled(config, _ENV_KEY, _CONFIG_KEY):
|
25
25
|
config.add_route(
|
26
26
|
"c2c_logging_level", config_utils.get_base_path(config) + r"/logging/level", request_method="GET"
|
27
27
|
)
|
@@ -29,7 +29,7 @@ def includeme(config: pyramid.config.Configurator) -> None:
|
|
29
29
|
_logging_change_level, route_name="c2c_logging_level", renderer="fast_json", http_cache=0
|
30
30
|
)
|
31
31
|
_restore_overrides(config)
|
32
|
-
|
32
|
+
_LOG.info("Enabled the /logging/level API")
|
33
33
|
|
34
34
|
|
35
35
|
def _logging_change_level(request: pyramid.request.Request) -> Mapping[str, Any]:
|
@@ -39,7 +39,7 @@ def _logging_change_level(request: pyramid.request.Request) -> Mapping[str, Any]
|
|
39
39
|
level = request.params.get("level")
|
40
40
|
logger = logging.getLogger(name)
|
41
41
|
if level is not None:
|
42
|
-
|
42
|
+
_LOG.critical(
|
43
43
|
"Logging of %s changed from %s to %s", name, logging.getLevelName(logger.level), level
|
44
44
|
)
|
45
45
|
_set_level(name=name, level=level)
|
@@ -63,20 +63,20 @@ def _set_level(name: str, level: str) -> bool:
|
|
63
63
|
def _restore_overrides(config: pyramid.config.Configurator) -> None:
|
64
64
|
try:
|
65
65
|
for name, level in _list_overrides(config.get_settings()):
|
66
|
-
|
66
|
+
_LOG.debug("Restoring logging level override for %s: %s", name, level)
|
67
67
|
logging.getLogger(name).setLevel(level)
|
68
68
|
except ImportError:
|
69
69
|
pass # don't have redis
|
70
70
|
except Exception: # pylint: disable=broad-except
|
71
71
|
# survive an error there. Logging levels is not business critical...
|
72
|
-
|
72
|
+
_LOG.warning("Cannot restore logging levels", exc_info=True)
|
73
73
|
|
74
74
|
|
75
75
|
def _store_override(settings: Mapping[str, Any], name: str, level: str) -> None:
|
76
76
|
try:
|
77
77
|
master, _, _ = redis_utils.get(settings)
|
78
78
|
if master:
|
79
|
-
master.set(
|
79
|
+
master.set(_REDIS_PREFIX + name, level)
|
80
80
|
except ImportError:
|
81
81
|
pass
|
82
82
|
|
@@ -84,8 +84,8 @@ def _store_override(settings: Mapping[str, Any], name: str, level: str) -> None:
|
|
84
84
|
def _list_overrides(settings: Mapping[str, Any]) -> Generator[tuple[str, str], None, None]:
|
85
85
|
_, slave, _ = redis_utils.get(settings)
|
86
86
|
if slave is not None:
|
87
|
-
for key in slave.scan_iter(
|
87
|
+
for key in slave.scan_iter(_REDIS_PREFIX + "*"):
|
88
88
|
level = slave.get(key)
|
89
|
-
name = key[len(
|
89
|
+
name = key[len(_REDIS_PREFIX) :]
|
90
90
|
if level is not None:
|
91
91
|
yield name, str(level)
|
c2cwsgiutils/models_graph.py
CHANGED
c2cwsgiutils/pretty_json.py
CHANGED
@@ -34,7 +34,6 @@ def init(config: pyramid.config.Configurator) -> None:
|
|
34
34
|
|
35
35
|
def includeme(config: pyramid.config.Configurator) -> None:
|
36
36
|
"""Initialize json and fast_json renderer."""
|
37
|
-
|
38
37
|
pretty_print = config_bool(
|
39
38
|
env_or_config(config, "C2C_JSON_PRETTY_PRINT", "c2c.json.pretty_print", "false")
|
40
39
|
)
|
c2cwsgiutils/prometheus.py
CHANGED
@@ -24,7 +24,6 @@ MULTI_PROCESS_COLLECTOR_BROADCAST_CHANNELS = [
|
|
24
24
|
|
25
25
|
def start(registry: Optional[prometheus_client.CollectorRegistry] = None) -> None:
|
26
26
|
"""Start separate HTTP server to provide the Prometheus metrics."""
|
27
|
-
|
28
27
|
if os.environ.get("C2C_PROMETHEUS_PORT") is not None:
|
29
28
|
broadcast.includeme()
|
30
29
|
|
@@ -38,20 +37,18 @@ def start(registry: Optional[prometheus_client.CollectorRegistry] = None) -> Non
|
|
38
37
|
|
39
38
|
def includeme(config: pyramid.config.Configurator) -> None:
|
40
39
|
"""Initialize prometheus_client in pyramid context."""
|
41
|
-
|
40
|
+
del config # unused
|
42
41
|
broadcast.subscribe("c2cwsgiutils_prometheus_collector_gc", _broadcast_collector_gc)
|
43
42
|
broadcast.subscribe("c2cwsgiutils_prometheus_collector_process", _broadcast_collector_process)
|
44
43
|
|
45
44
|
|
46
45
|
def build_metric_name(postfix: str) -> str:
|
47
46
|
"""Build the metric name with the prefix from the environment variable."""
|
48
|
-
|
49
47
|
return os.environ.get("C2C_PROMETHEUS_PREFIX", "c2cwsgiutils_") + postfix
|
50
48
|
|
51
49
|
|
52
50
|
def cleanup() -> None:
|
53
51
|
"""Cleanup the prometheus_client registry."""
|
54
|
-
|
55
52
|
redis_utils.cleanup()
|
56
53
|
broadcast.cleanup()
|
57
54
|
|
@@ -74,7 +71,6 @@ class SerializedMetric(TypedDict):
|
|
74
71
|
|
75
72
|
def _broadcast_collector_gc() -> list[SerializedMetric]:
|
76
73
|
"""Get the collected GC gauges."""
|
77
|
-
|
78
74
|
return serialize_collected_data(prometheus_client.GC_COLLECTOR)
|
79
75
|
|
80
76
|
|
@@ -85,7 +81,6 @@ def _broadcast_collector_process() -> list[SerializedMetric]:
|
|
85
81
|
|
86
82
|
def serialize_collected_data(collector: prometheus_client.registry.Collector) -> list[SerializedMetric]:
|
87
83
|
"""Serialize the data from the custom collector."""
|
88
|
-
|
89
84
|
gauges: list[SerializedMetric] = []
|
90
85
|
for process_gauge in collector.collect():
|
91
86
|
gauge: SerializedMetric = {
|
@@ -162,7 +157,6 @@ class MemoryMapCollector(prometheus_client.registry.Collector):
|
|
162
157
|
Initialize.
|
163
158
|
|
164
159
|
Arguments:
|
165
|
-
|
166
160
|
memory_type: can be rss, pss or size
|
167
161
|
pids: the list of pids or none
|
168
162
|
"""
|
c2cwsgiutils/pyramid.py
CHANGED
c2cwsgiutils/pyramid_logging.py
CHANGED
@@ -10,6 +10,7 @@ To add some info about requests:
|
|
10
10
|
|
11
11
|
A pyramid event handler is installed to setup this filter for the current request.
|
12
12
|
"""
|
13
|
+
|
13
14
|
import json
|
14
15
|
import logging
|
15
16
|
import logging.config
|
@@ -20,7 +21,7 @@ from typing import TYPE_CHECKING, Any, Optional, TextIO
|
|
20
21
|
import cee_syslog_handler
|
21
22
|
from pyramid.threadlocal import get_current_request
|
22
23
|
|
23
|
-
|
24
|
+
_LOG = logging.getLogger(__name__)
|
24
25
|
|
25
26
|
|
26
27
|
class _PyramidFilter(logging.Filter):
|
c2cwsgiutils/redis_stats.py
CHANGED
@@ -7,8 +7,8 @@ import pyramid.config
|
|
7
7
|
|
8
8
|
from c2cwsgiutils import config_utils, prometheus
|
9
9
|
|
10
|
-
|
11
|
-
|
10
|
+
_LOG = logging.getLogger(__name__)
|
11
|
+
_ORIG: Optional[Callable[..., Any]] = None
|
12
12
|
|
13
13
|
_PROMETHEUS_REDIS_SUMMARY = prometheus_client.Summary(
|
14
14
|
prometheus.build_metric_name("redis"),
|
@@ -19,9 +19,9 @@ _PROMETHEUS_REDIS_SUMMARY = prometheus_client.Summary(
|
|
19
19
|
|
20
20
|
|
21
21
|
def _execute_command_patch(self: Any, command: str, *args: Any, **options: Any) -> Any:
|
22
|
-
assert
|
22
|
+
assert _ORIG is not None
|
23
23
|
with _PROMETHEUS_REDIS_SUMMARY.labels(command=command).time():
|
24
|
-
return
|
24
|
+
return _ORIG(self, command, *args, **options)
|
25
25
|
|
26
26
|
|
27
27
|
def init(config: Optional[pyramid.config.Configurator] = None) -> None:
|
@@ -32,15 +32,15 @@ def init(config: Optional[pyramid.config.Configurator] = None) -> None:
|
|
32
32
|
|
33
33
|
def includeme(config: Optional[pyramid.config.Configurator] = None) -> None:
|
34
34
|
"""Initialize the Redis tracking."""
|
35
|
-
global
|
35
|
+
global _ORIG # pylint: disable=global-statement
|
36
36
|
if config_utils.env_or_config(
|
37
37
|
config, "C2C_TRACK_REDIS", "c2c.track_redis", True, config_utils.config_bool
|
38
38
|
):
|
39
39
|
try:
|
40
|
-
import redis.client
|
40
|
+
import redis.client # pylint: disable=import-outside-toplevel
|
41
41
|
|
42
|
-
|
42
|
+
_ORIG = redis.client.Redis.execute_command
|
43
43
|
redis.client.Redis.execute_command = _execute_command_patch # type: ignore
|
44
|
-
|
44
|
+
_LOG.info("Enabled the redis tracking")
|
45
45
|
except Exception: # pragma: nocover # pylint: disable=broad-except
|
46
|
-
|
46
|
+
_LOG.warning("Cannot enable redis tracking", exc_info=True)
|
c2cwsgiutils/redis_utils.py
CHANGED
@@ -11,19 +11,19 @@ import yaml
|
|
11
11
|
|
12
12
|
import c2cwsgiutils.config_utils
|
13
13
|
|
14
|
-
|
14
|
+
_LOG = logging.getLogger(__name__)
|
15
15
|
|
16
16
|
REDIS_URL_KEY = "C2C_REDIS_URL"
|
17
|
-
|
17
|
+
_REDIS_OPTIONS_KEY = "C2C_REDIS_OPTIONS"
|
18
18
|
REDIS_SENTINELS_KEY = "C2C_REDIS_SENTINELS"
|
19
19
|
REDIS_SERVICENAME_KEY = "C2C_REDIS_SERVICENAME"
|
20
|
-
|
20
|
+
_REDIS_DB_KEY = "C2C_REDIS_DB"
|
21
21
|
|
22
22
|
REDIS_URL_KEY_PROP = "c2c.redis_url"
|
23
|
-
|
23
|
+
_REDIS_OPTIONS_KEY_PROP = "c2c.redis_options"
|
24
24
|
REDIS_SENTINELS_KEY_PROP = "c2c.redis_sentinels"
|
25
25
|
REDIS_SERVICENAME_KEY_PROP = "c2c.redis_servicename"
|
26
|
-
|
26
|
+
_REDIS_DB_KEY_PROP = "c2c.redis_db"
|
27
27
|
|
28
28
|
_master: Optional["redis.client.Redis[str]"] = None
|
29
29
|
_slave: Optional["redis.client.Redis[str]"] = None
|
@@ -32,7 +32,7 @@ _sentinel: Optional[redis.sentinel.Sentinel] = None
|
|
32
32
|
|
33
33
|
def cleanup() -> None:
|
34
34
|
"""Cleanup the redis connections."""
|
35
|
-
global _master, _slave, _sentinel
|
35
|
+
global _master, _slave, _sentinel # pylint: disable=global-statement
|
36
36
|
_master = None
|
37
37
|
_slave = None
|
38
38
|
_sentinel = None
|
@@ -52,17 +52,17 @@ def get(
|
|
52
52
|
|
53
53
|
|
54
54
|
def _init(settings: Optional[Mapping[str, Any]]) -> None:
|
55
|
-
global _master, _slave, _sentinel
|
55
|
+
global _master, _slave, _sentinel # pylint: disable=global-statement
|
56
56
|
sentinels = c2cwsgiutils.config_utils.env_or_settings(
|
57
57
|
settings, REDIS_SENTINELS_KEY, REDIS_SENTINELS_KEY_PROP
|
58
58
|
)
|
59
59
|
service_name = c2cwsgiutils.config_utils.env_or_settings(
|
60
60
|
settings, REDIS_SERVICENAME_KEY, REDIS_SERVICENAME_KEY_PROP
|
61
61
|
)
|
62
|
-
db = c2cwsgiutils.config_utils.env_or_settings(settings,
|
62
|
+
db = c2cwsgiutils.config_utils.env_or_settings(settings, _REDIS_DB_KEY, _REDIS_DB_KEY_PROP)
|
63
63
|
url = c2cwsgiutils.config_utils.env_or_settings(settings, REDIS_URL_KEY, REDIS_URL_KEY_PROP)
|
64
64
|
redis_options_ = c2cwsgiutils.config_utils.env_or_settings(
|
65
|
-
settings,
|
65
|
+
settings, _REDIS_OPTIONS_KEY, _REDIS_OPTIONS_KEY_PROP
|
66
66
|
)
|
67
67
|
|
68
68
|
redis_options = (
|
@@ -82,16 +82,16 @@ def _init(settings: Optional[Mapping[str, Any]]) -> None:
|
|
82
82
|
db=db,
|
83
83
|
**redis_options,
|
84
84
|
)
|
85
|
-
|
85
|
+
_LOG.info("Redis setup using: %s, %s, %s", sentinels, service_name, redis_options_)
|
86
86
|
_master = _sentinel.master_for(service_name)
|
87
87
|
_slave = _sentinel.slave_for(service_name)
|
88
88
|
return
|
89
89
|
if url:
|
90
|
-
|
90
|
+
_LOG.info("Redis setup using: %s, with options: %s", url, redis_options_)
|
91
91
|
_master = redis.client.Redis.from_url(url, decode_responses=True, **redis_options)
|
92
92
|
_slave = _master
|
93
93
|
else:
|
94
|
-
|
94
|
+
_LOG.info(
|
95
95
|
"No Redis configuration found, use %s or %s to configure it", REDIS_URL_KEY, REDIS_SENTINELS_KEY
|
96
96
|
)
|
97
97
|
|
@@ -114,22 +114,23 @@ class PubSubWorkerThread(threading.Thread):
|
|
114
114
|
try:
|
115
115
|
pubsub.get_message(ignore_subscribe_messages=True, timeout=1)
|
116
116
|
if not last_was_ok:
|
117
|
-
|
117
|
+
_LOG.info("Redis is back")
|
118
118
|
last_was_ok = True
|
119
119
|
except redis.exceptions.RedisError:
|
120
120
|
if last_was_ok:
|
121
|
-
|
121
|
+
_LOG.warning("Redis connection problem")
|
122
122
|
last_was_ok = False
|
123
123
|
time.sleep(0.5)
|
124
124
|
except Exception: # pylint: disable=broad-except
|
125
|
-
|
126
|
-
|
125
|
+
_LOG.warning("Unexpected error", exc_info=True)
|
126
|
+
_LOG.info("Redis subscription worker stopped")
|
127
127
|
pubsub.close()
|
128
128
|
self._running = False
|
129
129
|
|
130
130
|
def stop(self) -> None:
|
131
|
-
|
132
|
-
#
|
131
|
+
"""Stop the worker."""
|
132
|
+
# Stopping simply unsubscribes from all channels and patterns.
|
133
|
+
# The unsubscribe responses that are generated will short circuit
|
133
134
|
# the loop in run(), calling pubsub.close() to clean up the connection
|
134
135
|
self.pubsub.unsubscribe()
|
135
136
|
self.pubsub.punsubscribe()
|