c2cwsgiutils 6.1.0.dev105__py3-none-any.whl → 6.1.1__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 +7 -8
- c2cwsgiutils/acceptance/package-lock.json +306 -213
- c2cwsgiutils/acceptance/package.json +2 -2
- c2cwsgiutils/acceptance/print.py +7 -3
- c2cwsgiutils/acceptance/utils.py +1 -3
- c2cwsgiutils/auth.py +27 -25
- c2cwsgiutils/broadcast/__init__.py +15 -16
- c2cwsgiutils/broadcast/interface.py +3 -3
- c2cwsgiutils/broadcast/local.py +1 -0
- c2cwsgiutils/broadcast/redis.py +13 -12
- c2cwsgiutils/client_info.py +19 -1
- c2cwsgiutils/coverage_setup.py +4 -3
- c2cwsgiutils/db.py +35 -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 -15
- c2cwsgiutils/health_check.py +24 -30
- c2cwsgiutils/index.py +7 -9
- 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 +1 -1
- c2cwsgiutils/redis_stats.py +9 -9
- c2cwsgiutils/redis_utils.py +19 -18
- c2cwsgiutils/request_tracking/__init__.py +13 -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 -1
- c2cwsgiutils/sql_profiler/__init__.py +5 -6
- c2cwsgiutils/sql_profiler/_impl.py +18 -17
- c2cwsgiutils/sqlalchemylogger/README.md +30 -13
- c2cwsgiutils/sqlalchemylogger/handlers.py +11 -10
- 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.1.0.dev105.dist-info → c2cwsgiutils-6.1.1.dist-info}/LICENSE +1 -1
- {c2cwsgiutils-6.1.0.dev105.dist-info → c2cwsgiutils-6.1.1.dist-info}/METADATA +17 -5
- c2cwsgiutils-6.1.1.dist-info/RECORD +67 -0
- {c2cwsgiutils-6.1.0.dev105.dist-info → c2cwsgiutils-6.1.1.dist-info}/WHEEL +1 -1
- c2cwsgiutils-6.1.0.dev105.dist-info/RECORD +0 -67
- {c2cwsgiutils-6.1.0.dev105.dist-info → c2cwsgiutils-6.1.1.dist-info}/entry_points.txt +0 -0
c2cwsgiutils/health_check.py
CHANGED
@@ -16,7 +16,7 @@ import traceback
|
|
16
16
|
from collections.abc import Mapping
|
17
17
|
from enum import Enum
|
18
18
|
from types import TracebackType
|
19
|
-
from typing import
|
19
|
+
from typing import Any, Callable, Literal, Optional, Union, cast
|
20
20
|
|
21
21
|
import prometheus_client
|
22
22
|
import pyramid.config
|
@@ -30,13 +30,10 @@ from pyramid.httpexceptions import HTTPNotFound
|
|
30
30
|
import c2cwsgiutils.db
|
31
31
|
from c2cwsgiutils import auth, broadcast, config_utils, prometheus, redis_utils, version
|
32
32
|
|
33
|
-
|
34
|
-
scoped_session = sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]
|
35
|
-
else:
|
36
|
-
scoped_session = sqlalchemy.orm.scoped_session
|
33
|
+
_scoped_session = sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]
|
37
34
|
|
38
|
-
|
39
|
-
|
35
|
+
_LOG = logging.getLogger(__name__)
|
36
|
+
_ALEMBIC_HEAD_RE = re.compile(r"^([a-f0-9]+) \(head\)\n$")
|
40
37
|
|
41
38
|
_PROMETHEUS_DB_SUMMARY = prometheus_client.Summary(
|
42
39
|
prometheus.build_metric_name("health_check_db"),
|
@@ -78,14 +75,16 @@ class JsonCheckException(Exception):
|
|
78
75
|
return self.message
|
79
76
|
|
80
77
|
def json_data(self) -> Any:
|
78
|
+
"""Return the JSON data to be returned in the response."""
|
81
79
|
return self.json
|
82
80
|
|
83
81
|
|
84
82
|
class _Binding:
|
85
83
|
def name(self) -> str:
|
84
|
+
"""Return the name of the binding."""
|
86
85
|
raise NotImplementedError()
|
87
86
|
|
88
|
-
def __enter__(self) ->
|
87
|
+
def __enter__(self) -> _scoped_session:
|
89
88
|
raise NotImplementedError()
|
90
89
|
|
91
90
|
def __exit__(
|
@@ -105,12 +104,12 @@ class _NewBinding(_Binding):
|
|
105
104
|
def name(self) -> str:
|
106
105
|
return self.session.engine_name(self.readwrite)
|
107
106
|
|
108
|
-
def __enter__(self) ->
|
107
|
+
def __enter__(self) -> _scoped_session:
|
109
108
|
return self.session(None, self.readwrite)
|
110
109
|
|
111
110
|
|
112
111
|
class _OldBinding(_Binding):
|
113
|
-
def __init__(self, session:
|
112
|
+
def __init__(self, session: _scoped_session, engine: sqlalchemy.engine.Engine):
|
114
113
|
self.session = session
|
115
114
|
self.engine = engine
|
116
115
|
self.prev_bind = None
|
@@ -118,7 +117,7 @@ class _OldBinding(_Binding):
|
|
118
117
|
def name(self) -> str:
|
119
118
|
return cast(str, self.engine.c2c_name) # type: ignore
|
120
119
|
|
121
|
-
def __enter__(self) ->
|
120
|
+
def __enter__(self) -> _scoped_session:
|
122
121
|
self.prev_bind = self.session.bind # type: ignore
|
123
122
|
self.session.bind = self.engine
|
124
123
|
return self.session
|
@@ -134,7 +133,7 @@ class _OldBinding(_Binding):
|
|
134
133
|
|
135
134
|
|
136
135
|
def _get_binding_class(
|
137
|
-
session: Union[
|
136
|
+
session: Union[_scoped_session, c2cwsgiutils.db.SessionFactory],
|
138
137
|
ro_engin: sqlalchemy.engine.Engine,
|
139
138
|
rw_engin: sqlalchemy.engine.Engine,
|
140
139
|
readwrite: bool,
|
@@ -146,7 +145,7 @@ def _get_binding_class(
|
|
146
145
|
|
147
146
|
|
148
147
|
def _get_bindings(
|
149
|
-
session: Union[
|
148
|
+
session: Union[_scoped_session, c2cwsgiutils.db.SessionFactory],
|
150
149
|
engine_type: EngineType,
|
151
150
|
) -> list[_Binding]:
|
152
151
|
if isinstance(session, c2cwsgiutils.db.SessionFactory):
|
@@ -184,7 +183,7 @@ def _get_alembic_version(alembic_ini_path: str, name: str) -> str:
|
|
184
183
|
out = subprocess.check_output( # nosec
|
185
184
|
["alembic", "--config", alembic_ini_path, "--name", name, "heads"], cwd=dirname, env=env
|
186
185
|
).decode("utf-8")
|
187
|
-
out_match =
|
186
|
+
out_match = _ALEMBIC_HEAD_RE.match(out)
|
188
187
|
if not out_match:
|
189
188
|
raise Exception( # pylint: disable=broad-exception-raised
|
190
189
|
"Cannot get the alembic HEAD version from: " + out
|
@@ -219,8 +218,8 @@ class HealthCheck:
|
|
219
218
|
|
220
219
|
def add_db_session_check(
|
221
220
|
self,
|
222
|
-
session: Union[
|
223
|
-
query_cb: Optional[Callable[[
|
221
|
+
session: Union[_scoped_session, c2cwsgiutils.db.SessionFactory],
|
222
|
+
query_cb: Optional[Callable[[_scoped_session], Any]] = None,
|
224
223
|
at_least_one_model: Optional[object] = None,
|
225
224
|
level: int = 1,
|
226
225
|
engine_type: EngineType = EngineType.READ_AND_WRITE,
|
@@ -229,7 +228,6 @@ class HealthCheck:
|
|
229
228
|
Check a DB session is working. You can specify either query_cb or at_least_one_model.
|
230
229
|
|
231
230
|
Arguments:
|
232
|
-
|
233
231
|
session: a DB session created by c2cwsgiutils.db.init()
|
234
232
|
query_cb: a callable that take a session as parameter and check it works
|
235
233
|
at_least_one_model: a model that must have at least one entry in the DB
|
@@ -247,7 +245,7 @@ class HealthCheck:
|
|
247
245
|
|
248
246
|
def add_alembic_check(
|
249
247
|
self,
|
250
|
-
session:
|
248
|
+
session: _scoped_session,
|
251
249
|
alembic_ini_path: str,
|
252
250
|
level: int = 2,
|
253
251
|
name: str = "alembic",
|
@@ -258,7 +256,6 @@ class HealthCheck:
|
|
258
256
|
Check the DB version against the HEAD version of Alembic.
|
259
257
|
|
260
258
|
Arguments:
|
261
|
-
|
262
259
|
session: A DB session created by c2cwsgiutils.db.init() giving access to the DB \
|
263
260
|
managed by Alembic
|
264
261
|
alembic_ini_path: Path to the Alembic INI file
|
@@ -283,7 +280,7 @@ class HealthCheck:
|
|
283
280
|
assert version_table
|
284
281
|
|
285
282
|
class _Check:
|
286
|
-
def __init__(self, session:
|
283
|
+
def __init__(self, session: _scoped_session) -> None:
|
287
284
|
self.session = session
|
288
285
|
|
289
286
|
def __call__(self, request: pyramid.request.Request) -> str:
|
@@ -334,7 +331,6 @@ class HealthCheck:
|
|
334
331
|
Check that a GET on an URL returns 2xx.
|
335
332
|
|
336
333
|
Arguments:
|
337
|
-
|
338
334
|
url: the URL to query or a function taking the request and returning it
|
339
335
|
params: the parameters or a function taking the request and returning them
|
340
336
|
headers: the headers or a function taking the request and returning them
|
@@ -367,7 +363,6 @@ class HealthCheck:
|
|
367
363
|
One such check is automatically added if the broadcaster is configured with redis.
|
368
364
|
|
369
365
|
Arguments:
|
370
|
-
|
371
366
|
name: the name of the check (defaults to url)
|
372
367
|
level: the level of the health check
|
373
368
|
"""
|
@@ -415,13 +410,13 @@ class HealthCheck:
|
|
415
410
|
"""
|
416
411
|
Check that the version matches across all instances.
|
417
412
|
|
418
|
-
|
419
|
-
|
413
|
+
Arguments:
|
420
414
|
name: the name of the check (defaults to "version")
|
421
415
|
level: the level of the health check
|
422
416
|
"""
|
423
417
|
|
424
418
|
def check(request: pyramid.request.Request) -> dict[str, Any]:
|
419
|
+
del request # unused
|
425
420
|
ref = version.get_version()
|
426
421
|
all_versions = _get_all_versions()
|
427
422
|
assert all_versions
|
@@ -443,7 +438,6 @@ class HealthCheck:
|
|
443
438
|
in the response. In case of failure it must raise an exception.
|
444
439
|
|
445
440
|
Arguments:
|
446
|
-
|
447
441
|
name: the name of the check
|
448
442
|
check_cb: the callback to call (takes the request as parameter)
|
449
443
|
level: the level of the health check
|
@@ -489,7 +483,7 @@ class HealthCheck:
|
|
489
483
|
_set_success(check_name=name)
|
490
484
|
except Exception as e: # pylint: disable=broad-except
|
491
485
|
_PROMETHEUS_HEALTH_CHECKS_FAILURE.labels(name=name).set(1)
|
492
|
-
|
486
|
+
_LOG.warning("Health check %s failed", name, exc_info=True)
|
493
487
|
failure = {"message": str(e), "timing": time.perf_counter() - start, "level": level}
|
494
488
|
if isinstance(e, JsonCheckException) and e.json_data() is not None:
|
495
489
|
failure["result"] = e.json_data()
|
@@ -500,9 +494,10 @@ class HealthCheck:
|
|
500
494
|
@staticmethod
|
501
495
|
def _create_db_engine_check(
|
502
496
|
binding: _Binding,
|
503
|
-
query_cb: Callable[[
|
497
|
+
query_cb: Callable[[_scoped_session], None],
|
504
498
|
) -> tuple[str, Callable[[pyramid.request.Request], None]]:
|
505
499
|
def check(request: pyramid.request.Request) -> None:
|
500
|
+
del request # unused
|
506
501
|
with binding as session:
|
507
502
|
with _PROMETHEUS_DB_SUMMARY.labels(
|
508
503
|
connection=binding.name(), check="database", configuration="<default>"
|
@@ -512,8 +507,8 @@ class HealthCheck:
|
|
512
507
|
return "db_engine_" + binding.name(), check
|
513
508
|
|
514
509
|
@staticmethod
|
515
|
-
def _at_least_one(model: Any) -> Callable[[
|
516
|
-
def query(session:
|
510
|
+
def _at_least_one(model: Any) -> Callable[[_scoped_session], Any]:
|
511
|
+
def query(session: _scoped_session) -> None:
|
517
512
|
result = session.query(model).first()
|
518
513
|
if result is None:
|
519
514
|
raise HTTPNotFound(model.__name__ + " record not found")
|
@@ -528,7 +523,6 @@ def _maybe_function(what: Any, request: pyramid.request.Request) -> Any:
|
|
528
523
|
@broadcast.decorator(expect_answers=False)
|
529
524
|
def _set_success(check_name: str) -> None:
|
530
525
|
"""Set check in success in all process."""
|
531
|
-
|
532
526
|
_PROMETHEUS_HEALTH_CHECKS_FAILURE.labels(name=check_name).set(0)
|
533
527
|
|
534
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
47
|
additional_title: Optional[str] = None
|
48
48
|
additional_noauth: list[str] = []
|
49
49
|
additional_auth: list[str] = []
|
50
|
-
|
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,8 +152,8 @@ 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)
|
@@ -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
@@ -21,7 +21,7 @@ from typing import TYPE_CHECKING, Any, Optional, TextIO
|
|
21
21
|
import cee_syslog_handler
|
22
22
|
from pyramid.threadlocal import get_current_request
|
23
23
|
|
24
|
-
|
24
|
+
_LOG = logging.getLogger(__name__)
|
25
25
|
|
26
26
|
|
27
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()
|
@@ -20,10 +20,10 @@ from pyramid.threadlocal import get_current_request
|
|
20
20
|
|
21
21
|
from c2cwsgiutils import config_utils, prometheus
|
22
22
|
|
23
|
-
|
23
|
+
_ID_HEADERS: list[str] = []
|
24
24
|
_HTTPAdapter_send = requests.adapters.HTTPAdapter.send
|
25
|
-
|
26
|
-
|
25
|
+
_LOG = logging.getLogger(__name__)
|
26
|
+
_DEFAULT_TIMEOUT: Optional[float] = None
|
27
27
|
_PROMETHEUS_REQUESTS_SUMMARY = prometheus_client.Summary(
|
28
28
|
prometheus.build_metric_name("requests"),
|
29
29
|
"Requests requests",
|
@@ -33,7 +33,7 @@ _PROMETHEUS_REQUESTS_SUMMARY = prometheus_client.Summary(
|
|
33
33
|
|
34
34
|
|
35
35
|
def _gen_request_id(request: pyramid.request.Request) -> str:
|
36
|
-
for id_header in
|
36
|
+
for id_header in _ID_HEADERS:
|
37
37
|
if id_header in request.headers:
|
38
38
|
return request.headers[id_header] # type: ignore
|
39
39
|
return str(uuid.uuid4())
|
@@ -50,15 +50,15 @@ def _patch_requests() -> None:
|
|
50
50
|
proxies: Optional[Mapping[str, str]] = None,
|
51
51
|
) -> requests.Response:
|
52
52
|
pyramid_request = get_current_request()
|
53
|
-
header =
|
53
|
+
header = _ID_HEADERS[0]
|
54
54
|
if pyramid_request is not None and header not in request.headers:
|
55
55
|
request.headers[header] = pyramid_request.c2c_request_id
|
56
56
|
|
57
57
|
if timeout is None:
|
58
|
-
if
|
59
|
-
timeout =
|
58
|
+
if _DEFAULT_TIMEOUT is not None:
|
59
|
+
timeout = _DEFAULT_TIMEOUT
|
60
60
|
else:
|
61
|
-
|
61
|
+
_LOG.warning("Doing a %s request without timeout to %s", request.method, request.url)
|
62
62
|
|
63
63
|
assert request.url
|
64
64
|
parsed = urllib.parse.urlparse(request.url)
|
@@ -94,20 +94,20 @@ def includeme(config: Optional[pyramid.config.Configurator] = None) -> None:
|
|
94
94
|
Use a X-Request-ID (or other) header to track all the logs related to a request
|
95
95
|
including on the sub services.
|
96
96
|
"""
|
97
|
-
global
|
98
|
-
|
97
|
+
global _ID_HEADERS, _DEFAULT_TIMEOUT # pylint: disable=global-statement
|
98
|
+
_ID_HEADERS = ["X-Request-ID", "X-Correlation-ID", "Request-ID", "X-Varnish", "X-Amzn-Trace-Id"]
|
99
99
|
if config is not None:
|
100
100
|
extra_header = config_utils.env_or_config(config, "C2C_REQUEST_ID_HEADER", "c2c.request_id_header")
|
101
101
|
if extra_header:
|
102
|
-
|
102
|
+
_ID_HEADERS.insert(0, extra_header)
|
103
103
|
config.add_request_method(_gen_request_id, "c2c_request_id", reify=True)
|
104
104
|
|
105
|
-
|
105
|
+
_DEFAULT_TIMEOUT = config_utils.env_or_config(
|
106
106
|
config, "C2C_REQUESTS_DEFAULT_TIMEOUT", "c2c.requests_default_timeout", type_=float
|
107
107
|
)
|
108
108
|
_patch_requests()
|
109
109
|
|
110
110
|
if config_utils.env_or_config(config, "C2C_SQL_REQUEST_ID", "c2c.sql_request_id", False):
|
111
|
-
from . import _sql
|
111
|
+
from . import _sql # pylint: disable=import-outside-toplevel
|
112
112
|
|
113
113
|
_sql.init()
|