c2cwsgiutils 6.1.0.dev105__py3-none-any.whl → 6.1.7__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 +17 -11
- 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 +2 -7
- c2cwsgiutils/debug/_views.py +20 -12
- c2cwsgiutils/debug/utils.py +9 -9
- c2cwsgiutils/errors.py +13 -15
- c2cwsgiutils/health_check.py +24 -30
- c2cwsgiutils/index.py +34 -13
- c2cwsgiutils/loader.py +21 -2
- c2cwsgiutils/logging_view.py +12 -12
- c2cwsgiutils/models_graph.py +0 -1
- c2cwsgiutils/pretty_json.py +0 -1
- c2cwsgiutils/prometheus.py +10 -10
- 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 +12 -11
- c2cwsgiutils/stats_pyramid/__init__.py +1 -5
- c2cwsgiutils/stats_pyramid/_db_spy.py +2 -2
- c2cwsgiutils/stats_pyramid/_pyramid_spy.py +12 -1
- c2cwsgiutils/templates/index.html.mako +4 -1
- c2cwsgiutils/version.py +11 -5
- {c2cwsgiutils-6.1.0.dev105.dist-info → c2cwsgiutils-6.1.7.dist-info}/LICENSE +1 -1
- {c2cwsgiutils-6.1.0.dev105.dist-info → c2cwsgiutils-6.1.7.dist-info}/METADATA +18 -6
- c2cwsgiutils-6.1.7.dist-info/RECORD +67 -0
- {c2cwsgiutils-6.1.0.dev105.dist-info → c2cwsgiutils-6.1.7.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.7.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,17 +113,29 @@ 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()
|
122
122
|
if type_ is None:
|
123
123
|
if isinstance(value, int):
|
124
124
|
type_ = "number"
|
125
|
+
elif isinstance(value, bool):
|
126
|
+
type_ = "checkbox"
|
125
127
|
else:
|
126
128
|
type_ = "text"
|
129
|
+
if type_ == "checkbox":
|
130
|
+
checked = " checked" if value else ""
|
131
|
+
return f"""
|
132
|
+
<div class="form-check">
|
133
|
+
<input class="form-check-input" type="checkbox" name="{name}" value="true" id="{id_}"{checked}>
|
134
|
+
<label class="form-check-label" for="{id_}">
|
135
|
+
{label}
|
136
|
+
</label>
|
137
|
+
</div>"""
|
138
|
+
|
127
139
|
result = ""
|
128
140
|
if label is not None:
|
129
141
|
result += f'<div class="row mb-3"><label class="col-sm-2 col-form-label" for="{id_}">{label}</label>'
|
@@ -138,7 +150,6 @@ def input_(
|
|
138
150
|
|
139
151
|
def button(label: str) -> str:
|
140
152
|
"""Get en HTML button."""
|
141
|
-
|
142
153
|
return f'<button class="btn btn-primary" type="submit">{label}</button>'
|
143
154
|
|
144
155
|
|
@@ -153,8 +164,8 @@ def _index(request: pyramid.request.Request) -> dict[str, str]:
|
|
153
164
|
body = ""
|
154
165
|
body += _health_check(request)
|
155
166
|
body += _stats(request)
|
156
|
-
body += _versions(request)
|
157
167
|
if has_access:
|
168
|
+
body += _versions(request)
|
158
169
|
body += _debug(request)
|
159
170
|
body += _db_maintenance(request)
|
160
171
|
body += _logging(request)
|
@@ -202,7 +213,7 @@ def _index(request: pyramid.request.Request) -> dict[str, str]:
|
|
202
213
|
def _versions(request: pyramid.request.Request) -> str:
|
203
214
|
versions_url = _url(request, "c2c_versions")
|
204
215
|
if versions_url:
|
205
|
-
return section("Versions"
|
216
|
+
return section("Versions " + link(versions_url, "Get"), sep=False)
|
206
217
|
else:
|
207
218
|
return ""
|
208
219
|
|
@@ -281,6 +292,7 @@ def _logging(request: pyramid.request.Request) -> str:
|
|
281
292
|
def _debug(request: pyramid.request.Request) -> str:
|
282
293
|
dump_memory_url = _url(request, "c2c_debug_memory")
|
283
294
|
if dump_memory_url:
|
295
|
+
as_dot = 'as <a href="https://graphviz.org/">dot diagram</a>, can be open with <a href="https://pypi.org/project/xdot/">xdot</a>'
|
284
296
|
return section(
|
285
297
|
" ".join(
|
286
298
|
[
|
@@ -290,32 +302,42 @@ def _debug(request: pyramid.request.Request) -> str:
|
|
290
302
|
link(_url(request, "c2c_debug_memory_maps"), "Mapped memory"),
|
291
303
|
]
|
292
304
|
),
|
305
|
+
'<h2>Memory usage<span style="font-size: 0.5em;">, with <a href="https://mg.pov.lt/objgraph/">objgraph</a></span></h2>',
|
306
|
+
"<p>Runs the garbage collector and dumps the memory usage as JSON.</p>",
|
293
307
|
form(
|
294
308
|
dump_memory_url,
|
295
309
|
input_("limit", value=30),
|
296
310
|
input_("analyze_type"),
|
311
|
+
input_("python_internals_map", type_="checkbox"),
|
297
312
|
button("Dump memory usage"),
|
298
313
|
),
|
314
|
+
f"<p>Runs the garbage collector and dumps the memory refs {as_dot}.</p>",
|
299
315
|
form(
|
300
316
|
_url(request, "c2c_debug_show_refs"),
|
301
317
|
input_("analyze_type", value="gunicorn.app.wsgiapp.WSGIApplication"),
|
302
|
-
input_("
|
303
|
-
input_("
|
318
|
+
input_("analyze_id", type_="number"),
|
319
|
+
input_("max_depth", type_="number", value=3),
|
320
|
+
input_("too_many", type_="number", value=10),
|
304
321
|
input_("min_size_kb", type_="number"),
|
305
322
|
button("Object refs"),
|
306
323
|
),
|
324
|
+
"<p>Runs the garbage collector, query the path, runs the garbage collector again, get the memory diff as JSON.</p>",
|
307
325
|
form(
|
308
326
|
_url(request, "c2c_debug_memory_diff"),
|
309
327
|
input_("path"),
|
310
328
|
input_("limit", value=30),
|
329
|
+
input_("no_warmup", type_="checkbox"),
|
311
330
|
button("Memory diff"),
|
312
331
|
),
|
332
|
+
"<h2>Sleep</h2>",
|
313
333
|
form(
|
314
334
|
_url(request, "c2c_debug_sleep"),
|
315
335
|
input_("time", value=1),
|
316
336
|
button("Sleep"),
|
317
337
|
),
|
318
|
-
|
338
|
+
"<h2>Server times</h2>",
|
339
|
+
form(_url(request, "c2c_debug_time"), button("Get")),
|
340
|
+
"<h2>HTTP error</h2>",
|
319
341
|
form(
|
320
342
|
_url(request, "c2c_debug_error"),
|
321
343
|
input_("status", value=500),
|
@@ -346,7 +368,6 @@ def _health_check(request: pyramid.request.Request) -> str:
|
|
346
368
|
|
347
369
|
def _github_login(request: pyramid.request.Request) -> dict[str, Any]:
|
348
370
|
"""Get the view that start the authentication on GitHub."""
|
349
|
-
|
350
371
|
settings = request.registry.settings
|
351
372
|
params = dict(request.params)
|
352
373
|
callback_url = _url(
|
@@ -500,7 +521,7 @@ def includeme(config: pyramid.config.Configurator) -> None:
|
|
500
521
|
settings = config.get_settings()
|
501
522
|
auth_type_ = auth_type(settings)
|
502
523
|
if auth_type_ == AuthenticationType.SECRET:
|
503
|
-
|
524
|
+
_LOG.warning(
|
504
525
|
"It is recommended to use OAuth2 with GitHub login instead of the `C2C_SECRET` because it "
|
505
526
|
"protects from brute force attacks and the access grant is personal and can be revoked."
|
506
527
|
)
|
c2cwsgiutils/loader.py
CHANGED
@@ -3,9 +3,9 @@ from typing import Optional, cast
|
|
3
3
|
|
4
4
|
from plaster_pastedeploy import Loader as BaseLoader
|
5
5
|
|
6
|
-
from c2cwsgiutils import get_config_defaults
|
6
|
+
from c2cwsgiutils import get_config_defaults, get_logconfig_dict
|
7
7
|
|
8
|
-
|
8
|
+
_LOG = logging.getLogger(__name__)
|
9
9
|
|
10
10
|
|
11
11
|
class Loader(BaseLoader): # type: ignore
|
@@ -19,3 +19,22 @@ class Loader(BaseLoader): # type: ignore
|
|
19
19
|
def __repr__(self) -> str:
|
20
20
|
"""Get the object representation."""
|
21
21
|
return f'c2cwsgiutils.loader.Loader(uri="{self.uri}")'
|
22
|
+
|
23
|
+
def setup_logging(self, defaults: Optional[dict[str, str]] = None) -> None:
|
24
|
+
"""
|
25
|
+
Set up logging via :func:`logging.config.dictConfig` with value returned from c2cwsgiutils.get_logconfig_dict.
|
26
|
+
|
27
|
+
Defaults are specified for the special ``__file__`` and ``here``
|
28
|
+
variables, similar to PasteDeploy config loading. Extra defaults can
|
29
|
+
optionally be specified as a dict in ``defaults``.
|
30
|
+
|
31
|
+
Arguments:
|
32
|
+
---------
|
33
|
+
defaults: The defaults that will be used when passed to
|
34
|
+
:func:`logging.config.fileConfig`.
|
35
|
+
|
36
|
+
"""
|
37
|
+
if "loggers" in self.get_sections():
|
38
|
+
logging.config.dictConfig(get_logconfig_dict(self.uri.path))
|
39
|
+
else:
|
40
|
+
logging.basicConfig()
|
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
@@ -1,5 +1,6 @@
|
|
1
1
|
"""Every thing we needs to have the metrics in Prometheus."""
|
2
2
|
|
3
|
+
import logging
|
3
4
|
import os
|
4
5
|
import re
|
5
6
|
from collections.abc import Generator, Iterable
|
@@ -11,10 +12,12 @@ import prometheus_client.metrics_core
|
|
11
12
|
import prometheus_client.multiprocess
|
12
13
|
import prometheus_client.registry
|
13
14
|
import pyramid.config
|
15
|
+
import redis.exceptions
|
14
16
|
|
15
17
|
from c2cwsgiutils import broadcast, redis_utils
|
16
18
|
from c2cwsgiutils.debug.utils import dump_memory_maps
|
17
19
|
|
20
|
+
_LOG = logging.getLogger(__name__)
|
18
21
|
_NUMBER_RE = re.compile(r"^[0-9]+$")
|
19
22
|
MULTI_PROCESS_COLLECTOR_BROADCAST_CHANNELS = [
|
20
23
|
"c2cwsgiutils_prometheus_collector_gc",
|
@@ -24,7 +27,6 @@ MULTI_PROCESS_COLLECTOR_BROADCAST_CHANNELS = [
|
|
24
27
|
|
25
28
|
def start(registry: Optional[prometheus_client.CollectorRegistry] = None) -> None:
|
26
29
|
"""Start separate HTTP server to provide the Prometheus metrics."""
|
27
|
-
|
28
30
|
if os.environ.get("C2C_PROMETHEUS_PORT") is not None:
|
29
31
|
broadcast.includeme()
|
30
32
|
|
@@ -38,20 +40,18 @@ def start(registry: Optional[prometheus_client.CollectorRegistry] = None) -> Non
|
|
38
40
|
|
39
41
|
def includeme(config: pyramid.config.Configurator) -> None:
|
40
42
|
"""Initialize prometheus_client in pyramid context."""
|
41
|
-
|
43
|
+
del config # unused
|
42
44
|
broadcast.subscribe("c2cwsgiutils_prometheus_collector_gc", _broadcast_collector_gc)
|
43
45
|
broadcast.subscribe("c2cwsgiutils_prometheus_collector_process", _broadcast_collector_process)
|
44
46
|
|
45
47
|
|
46
48
|
def build_metric_name(postfix: str) -> str:
|
47
49
|
"""Build the metric name with the prefix from the environment variable."""
|
48
|
-
|
49
50
|
return os.environ.get("C2C_PROMETHEUS_PREFIX", "c2cwsgiutils_") + postfix
|
50
51
|
|
51
52
|
|
52
53
|
def cleanup() -> None:
|
53
54
|
"""Cleanup the prometheus_client registry."""
|
54
|
-
|
55
55
|
redis_utils.cleanup()
|
56
56
|
broadcast.cleanup()
|
57
57
|
|
@@ -74,7 +74,6 @@ class SerializedMetric(TypedDict):
|
|
74
74
|
|
75
75
|
def _broadcast_collector_gc() -> list[SerializedMetric]:
|
76
76
|
"""Get the collected GC gauges."""
|
77
|
-
|
78
77
|
return serialize_collected_data(prometheus_client.GC_COLLECTOR)
|
79
78
|
|
80
79
|
|
@@ -85,7 +84,6 @@ def _broadcast_collector_process() -> list[SerializedMetric]:
|
|
85
84
|
|
86
85
|
def serialize_collected_data(collector: prometheus_client.registry.Collector) -> list[SerializedMetric]:
|
87
86
|
"""Serialize the data from the custom collector."""
|
88
|
-
|
89
87
|
gauges: list[SerializedMetric] = []
|
90
88
|
for process_gauge in collector.collect():
|
91
89
|
gauge: SerializedMetric = {
|
@@ -122,9 +120,12 @@ class MultiProcessCustomCollector(prometheus_client.registry.Collector):
|
|
122
120
|
def collect(self) -> Generator[prometheus_client.core.Metric, None, None]:
|
123
121
|
results: list[list[SerializedMetric]] = []
|
124
122
|
for channel in MULTI_PROCESS_COLLECTOR_BROADCAST_CHANNELS:
|
125
|
-
|
126
|
-
|
127
|
-
|
123
|
+
try:
|
124
|
+
result = broadcast.broadcast(channel, expect_answers=True)
|
125
|
+
if result is not None:
|
126
|
+
results.extend(cast(Iterable[list[SerializedMetric]], result))
|
127
|
+
except redis.exceptions.ConnectionError:
|
128
|
+
_LOG.error("Failed to get the metrics from the other processes")
|
128
129
|
return _deserialize_collected_data(results)
|
129
130
|
|
130
131
|
|
@@ -162,7 +163,6 @@ class MemoryMapCollector(prometheus_client.registry.Collector):
|
|
162
163
|
Initialize.
|
163
164
|
|
164
165
|
Arguments:
|
165
|
-
|
166
166
|
memory_type: can be rss, pss or size
|
167
167
|
pids: the list of pids or none
|
168
168
|
"""
|
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)
|