c2cwsgiutils 5.1.7.dev20230901073305__py3-none-any.whl → 5.2.1.dev197__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- c2cwsgiutils/__init__.py +13 -13
- c2cwsgiutils/acceptance/connection.py +5 -2
- c2cwsgiutils/acceptance/image.py +98 -4
- c2cwsgiutils/acceptance/package-lock.json +1933 -0
- c2cwsgiutils/acceptance/package.json +7 -0
- c2cwsgiutils/acceptance/print.py +4 -4
- c2cwsgiutils/acceptance/screenshot.js +62 -0
- c2cwsgiutils/acceptance/utils.py +14 -22
- c2cwsgiutils/auth.py +4 -4
- c2cwsgiutils/broadcast/__init__.py +15 -7
- c2cwsgiutils/broadcast/interface.py +3 -2
- c2cwsgiutils/broadcast/local.py +3 -2
- c2cwsgiutils/broadcast/redis.py +8 -7
- c2cwsgiutils/client_info.py +5 -5
- c2cwsgiutils/config_utils.py +2 -1
- c2cwsgiutils/coverage_setup.py +2 -2
- c2cwsgiutils/db.py +58 -37
- c2cwsgiutils/db_maintenance_view.py +2 -1
- c2cwsgiutils/debug/_listeners.py +10 -9
- c2cwsgiutils/debug/_views.py +12 -11
- c2cwsgiutils/debug/utils.py +5 -5
- c2cwsgiutils/errors.py +7 -6
- c2cwsgiutils/health_check.py +96 -85
- c2cwsgiutils/index.py +90 -105
- c2cwsgiutils/loader.py +3 -3
- c2cwsgiutils/logging_view.py +3 -2
- c2cwsgiutils/models_graph.py +8 -6
- c2cwsgiutils/prometheus.py +175 -57
- c2cwsgiutils/pyramid.py +4 -2
- c2cwsgiutils/pyramid_logging.py +2 -1
- c2cwsgiutils/redis_stats.py +13 -11
- c2cwsgiutils/redis_utils.py +15 -14
- c2cwsgiutils/request_tracking/__init__.py +36 -30
- c2cwsgiutils/request_tracking/_sql.py +3 -1
- c2cwsgiutils/scripts/genversion.py +4 -4
- c2cwsgiutils/scripts/stats_db.py +130 -68
- c2cwsgiutils/scripts/test_print.py +1 -1
- c2cwsgiutils/sentry.py +2 -1
- c2cwsgiutils/setup_process.py +13 -17
- c2cwsgiutils/sql_profiler/_impl.py +12 -5
- c2cwsgiutils/sqlalchemylogger/README.md +48 -0
- c2cwsgiutils/sqlalchemylogger/_models.py +7 -4
- c2cwsgiutils/sqlalchemylogger/examples/example.py +15 -0
- c2cwsgiutils/sqlalchemylogger/handlers.py +11 -8
- c2cwsgiutils/static/favicon-16x16.png +0 -0
- c2cwsgiutils/static/favicon-32x32.png +0 -0
- c2cwsgiutils/stats_pyramid/__init__.py +7 -11
- c2cwsgiutils/stats_pyramid/_db_spy.py +14 -11
- c2cwsgiutils/stats_pyramid/_pyramid_spy.py +29 -20
- c2cwsgiutils/templates/index.html.mako +50 -0
- c2cwsgiutils/version.py +49 -16
- c2cwsgiutils-5.2.1.dev197.dist-info/LICENSE +22 -0
- {c2cwsgiutils-5.1.7.dev20230901073305.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/METADATA +187 -135
- c2cwsgiutils-5.2.1.dev197.dist-info/RECORD +67 -0
- {c2cwsgiutils-5.1.7.dev20230901073305.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/WHEEL +1 -2
- c2cwsgiutils-5.2.1.dev197.dist-info/entry_points.txt +21 -0
- c2cwsgiutils/acceptance/composition.py +0 -129
- c2cwsgiutils/metrics.py +0 -110
- c2cwsgiutils/scripts/check_es.py +0 -130
- c2cwsgiutils/scripts/coverage_report.py +0 -36
- c2cwsgiutils/stats.py +0 -355
- c2cwsgiutils/stats_pyramid/_views.py +0 -16
- c2cwsgiutils-5.1.7.dev20230901073305.data/scripts/c2cwsgiutils-run +0 -32
- c2cwsgiutils-5.1.7.dev20230901073305.dist-info/LICENSE.txt +0 -28
- c2cwsgiutils-5.1.7.dev20230901073305.dist-info/RECORD +0 -69
- c2cwsgiutils-5.1.7.dev20230901073305.dist-info/entry_points.txt +0 -25
- c2cwsgiutils-5.1.7.dev20230901073305.dist-info/top_level.txt +0 -2
- tests/acceptance/__init__.py +0 -0
- tests/acceptance/test_utils.py +0 -13
@@ -1,15 +1,24 @@
|
|
1
1
|
import logging
|
2
2
|
import re
|
3
|
-
|
3
|
+
import time
|
4
|
+
from typing import Any, Callable
|
4
5
|
|
6
|
+
import prometheus_client
|
5
7
|
import sqlalchemy.event
|
6
8
|
from sqlalchemy.engine import Connection, Engine
|
7
9
|
from sqlalchemy.orm import Session
|
8
10
|
|
9
|
-
from c2cwsgiutils import
|
11
|
+
from c2cwsgiutils import prometheus
|
10
12
|
|
11
13
|
LOG = logging.getLogger(__name__)
|
12
14
|
|
15
|
+
_PROMETHEUS_DB_SUMMARY = prometheus_client.Summary(
|
16
|
+
prometheus.build_metric_name("database"),
|
17
|
+
"Database requests",
|
18
|
+
["what"],
|
19
|
+
unit="seconds",
|
20
|
+
)
|
21
|
+
|
13
22
|
|
14
23
|
def _jump_string(content: str, pos: int) -> int:
|
15
24
|
quote_char = content[pos]
|
@@ -58,17 +67,11 @@ def _simplify_sql(sql: str) -> str:
|
|
58
67
|
|
59
68
|
|
60
69
|
def _create_sqlalchemy_timer_cb(what: str) -> Callable[..., Any]:
|
61
|
-
|
62
|
-
key = ["sql"]
|
63
|
-
tags: Optional[Dict[str, str]] = dict(query=what)
|
64
|
-
else:
|
65
|
-
key = ["sql", what]
|
66
|
-
tags = None
|
67
|
-
measure = stats.timer(key, tags)
|
70
|
+
start = time.perf_counter()
|
68
71
|
|
69
72
|
def after(*_args: Any, **_kwargs: Any) -> None:
|
70
|
-
|
71
|
-
LOG.debug("Execute statement '%s' in %d.", what,
|
73
|
+
_PROMETHEUS_DB_SUMMARY.labels({"query": what}).observe(time.perf_counter() - start)
|
74
|
+
LOG.debug("Execute statement '%s' in %d.", what, time.perf_counter() - start)
|
72
75
|
|
73
76
|
return after
|
74
77
|
|
@@ -1,11 +1,26 @@
|
|
1
|
-
|
1
|
+
import time
|
2
|
+
from typing import Callable, Optional
|
2
3
|
|
4
|
+
import prometheus_client
|
3
5
|
import pyramid.config
|
4
6
|
import pyramid.events
|
5
7
|
import pyramid.request
|
6
8
|
from pyramid.httpexceptions import HTTPException
|
7
9
|
|
8
|
-
from c2cwsgiutils import
|
10
|
+
from c2cwsgiutils import prometheus
|
11
|
+
|
12
|
+
_PROMETHEUS_PYRAMID_ROUTES_SUMMARY = prometheus_client.Summary(
|
13
|
+
prometheus.build_metric_name("pyramid_routes"),
|
14
|
+
"Pyramid routes",
|
15
|
+
["method", "route", "status", "group"],
|
16
|
+
unit="seconds",
|
17
|
+
)
|
18
|
+
_PROMETHEUS_PYRAMID_VIEWS_SUMMARY = prometheus_client.Summary(
|
19
|
+
prometheus.build_metric_name("pyramid_render"),
|
20
|
+
"Pyramid render",
|
21
|
+
["method", "route", "status", "group"],
|
22
|
+
unit="seconds",
|
23
|
+
)
|
9
24
|
|
10
25
|
|
11
26
|
def _add_server_metric(
|
@@ -14,10 +29,10 @@ def _add_server_metric(
|
|
14
29
|
duration: Optional[float] = None,
|
15
30
|
description: Optional[str] = None,
|
16
31
|
) -> None:
|
17
|
-
# format: <name>;
|
32
|
+
# format: <name>;due=<duration>;desc=<description>
|
18
33
|
metric = name
|
19
34
|
if duration is not None:
|
20
|
-
metric += ";
|
35
|
+
metric += ";due=" + str(round(duration * 1000))
|
21
36
|
if description is not None:
|
22
37
|
metric += ";desc=" + description
|
23
38
|
|
@@ -28,14 +43,16 @@ def _add_server_metric(
|
|
28
43
|
|
29
44
|
|
30
45
|
def _create_finished_cb(
|
31
|
-
kind: str, measure:
|
46
|
+
kind: str, measure: prometheus_client.Summary
|
32
47
|
) -> Callable[[pyramid.request.Request], None]: # pragma: nocover
|
48
|
+
start = time.process_time()
|
49
|
+
|
33
50
|
def finished_cb(request: pyramid.request.Request) -> None:
|
34
51
|
if request.exception is not None:
|
35
52
|
if isinstance(request.exception, HTTPException):
|
36
53
|
status = request.exception.code
|
37
54
|
else:
|
38
|
-
status =
|
55
|
+
status = 599
|
39
56
|
else:
|
40
57
|
status = request.response.status_code
|
41
58
|
if request.matched_route is None:
|
@@ -44,32 +61,24 @@ def _create_finished_cb(
|
|
44
61
|
name = request.matched_route.name
|
45
62
|
if kind == "route":
|
46
63
|
_add_server_metric(request, "route", description=name)
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
)
|
52
|
-
else:
|
53
|
-
key = [kind, request.method, name, status]
|
54
|
-
tags = None
|
55
|
-
duration = measure.stop(key, tags)
|
56
|
-
_add_server_metric(request, kind, duration=duration)
|
64
|
+
measure.labels(
|
65
|
+
method=request.method, route=name, status=status, group=str(status // 100 * 100)
|
66
|
+
).observe(time.process_time() - start)
|
67
|
+
_add_server_metric(request, kind, duration=time.process_time() - start)
|
57
68
|
|
58
69
|
return finished_cb
|
59
70
|
|
60
71
|
|
61
72
|
def _request_callback(event: pyramid.events.NewRequest) -> None: # pragma: nocover
|
62
73
|
"""Finish the callback called when a new HTTP request is incoming."""
|
63
|
-
|
64
|
-
event.request.add_finished_callback(_create_finished_cb("route", measure))
|
74
|
+
event.request.add_finished_callback(_create_finished_cb("route", _PROMETHEUS_PYRAMID_ROUTES_SUMMARY))
|
65
75
|
|
66
76
|
|
67
77
|
def _before_rendered_callback(event: pyramid.events.BeforeRender) -> None: # pragma: nocover
|
68
78
|
"""Finish the callback called when the rendering is starting."""
|
69
79
|
request = event.get("request", None)
|
70
80
|
if request:
|
71
|
-
|
72
|
-
request.add_finished_callback(_create_finished_cb("render", measure))
|
81
|
+
request.add_finished_callback(_create_finished_cb("render", _PROMETHEUS_PYRAMID_VIEWS_SUMMARY))
|
73
82
|
|
74
83
|
|
75
84
|
def init(config: pyramid.config.Configurator) -> None: # pragma: nocover
|
@@ -0,0 +1,50 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8" />
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
6
|
+
<link
|
7
|
+
rel="icon"
|
8
|
+
type="image/png"
|
9
|
+
sizes="32x32"
|
10
|
+
href="${request.static_url('c2cwsgiutils:static/favicon-32x32.png')}"
|
11
|
+
referrerpolicy="no-referrer"
|
12
|
+
/>
|
13
|
+
<link
|
14
|
+
rel="icon"
|
15
|
+
type="image/png"
|
16
|
+
sizes="16x16"
|
17
|
+
href="${request.static_url('c2cwsgiutils:static/favicon-16x16.png')}"
|
18
|
+
referrerpolicy="no-referrer"
|
19
|
+
/>
|
20
|
+
<link
|
21
|
+
rel="stylesheet"
|
22
|
+
href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css"
|
23
|
+
integrity="sha512-b2QcS5SsA8tZodcDtGRELiGv5SaKSk1vDHDaQRda0htPYWZ6046lr3kJ5bAAQdpV2mmA/4v0wQF9MyU6/pDIAg=="
|
24
|
+
crossorigin="anonymous"
|
25
|
+
referrerpolicy="no-referrer"
|
26
|
+
/>
|
27
|
+
<title>c2cwsgiutils tools</title>
|
28
|
+
<style>
|
29
|
+
body {
|
30
|
+
margin-top: 0.5rem;
|
31
|
+
}
|
32
|
+
button, p {
|
33
|
+
margin-bottom: 0.5rem;
|
34
|
+
}
|
35
|
+
</style>
|
36
|
+
</head>
|
37
|
+
<body>
|
38
|
+
<script>
|
39
|
+
(() => {
|
40
|
+
'use strict'
|
41
|
+
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
42
|
+
document.documentElement.setAttribute('data-bs-theme', 'dark');
|
43
|
+
}
|
44
|
+
})()
|
45
|
+
</script>
|
46
|
+
<div class="container-fluid">
|
47
|
+
${ body | n }
|
48
|
+
</div>
|
49
|
+
</body>
|
50
|
+
</html>
|
c2cwsgiutils/version.py
CHANGED
@@ -1,15 +1,38 @@
|
|
1
1
|
import json
|
2
2
|
import logging
|
3
3
|
import os
|
4
|
+
import re
|
4
5
|
import warnings
|
5
|
-
from typing import
|
6
|
+
from typing import Optional, cast
|
6
7
|
|
8
|
+
import prometheus_client
|
7
9
|
import pyramid.config
|
8
10
|
|
9
|
-
from c2cwsgiutils import config_utils,
|
11
|
+
from c2cwsgiutils import config_utils, prometheus
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
+
_VERSIONS_PATH = "/app/versions.json"
|
14
|
+
_LOG = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
_PACKAGES = os.environ.get("C2C_PROMETHEUS_PACKAGES", "c2cwsgiutils,pyramid,gunicorn,SQLAlchemy").split(",")
|
17
|
+
_APPLICATION_PACKAGES = os.environ.get("C2C_PROMETHEUS_APPLICATION_PACKAGE")
|
18
|
+
_LABEL_RE_NOT_ALLOWED = re.compile(r"[^a-zA-Z0-9]+")
|
19
|
+
|
20
|
+
|
21
|
+
def _sanitize_label(label: str) -> str:
|
22
|
+
# Replace chart that nor a-zA-Z0-9 with _
|
23
|
+
return _LABEL_RE_NOT_ALLOWED.sub("_", label)
|
24
|
+
|
25
|
+
|
26
|
+
_PROMETHEUS_VERSIONS_INFO = prometheus_client.Gauge(
|
27
|
+
prometheus.build_metric_name("version"),
|
28
|
+
"The version of the application",
|
29
|
+
labelnames=[
|
30
|
+
"git_hash",
|
31
|
+
*[_sanitize_label(p) for p in _PACKAGES],
|
32
|
+
*([] if _APPLICATION_PACKAGES is None else ["application"]),
|
33
|
+
],
|
34
|
+
multiprocess_mode="liveall",
|
35
|
+
)
|
13
36
|
|
14
37
|
|
15
38
|
def init(config: pyramid.config.Configurator) -> None:
|
@@ -20,7 +43,8 @@ def init(config: pyramid.config.Configurator) -> None:
|
|
20
43
|
|
21
44
|
def includeme(config: pyramid.config.Configurator) -> None:
|
22
45
|
"""Initialize the versions view."""
|
23
|
-
|
46
|
+
|
47
|
+
if os.path.isfile(_VERSIONS_PATH):
|
24
48
|
versions = _read_versions()
|
25
49
|
config.add_route(
|
26
50
|
"c2c_versions", config_utils.get_base_path(config) + r"/versions.json", request_method="GET"
|
@@ -28,30 +52,39 @@ def includeme(config: pyramid.config.Configurator) -> None:
|
|
28
52
|
config.add_view(
|
29
53
|
lambda request: versions, route_name="c2c_versions", renderer="fast_json", http_cache=0
|
30
54
|
)
|
31
|
-
|
55
|
+
_LOG.info("Installed the /versions.json service")
|
32
56
|
git_hash = versions["main"]["git_hash"]
|
33
57
|
|
34
58
|
if "git_tag" in versions["main"]:
|
35
|
-
|
59
|
+
_LOG.info("Starting version %s (%s)", versions["main"]["git_tag"], git_hash)
|
36
60
|
else:
|
37
|
-
|
61
|
+
_LOG.info("Starting version %s", git_hash)
|
38
62
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
63
|
+
labels = {
|
64
|
+
"git_hash": git_hash,
|
65
|
+
**{
|
66
|
+
_sanitize_label(package): versions["packages"].get(package, "<missing>")
|
67
|
+
for package in _PACKAGES
|
68
|
+
},
|
69
|
+
**(
|
70
|
+
{}
|
71
|
+
if _APPLICATION_PACKAGES is None
|
72
|
+
else {"application": versions["packages"].get(_APPLICATION_PACKAGES, "<missing>")}
|
73
|
+
),
|
74
|
+
}
|
75
|
+
_PROMETHEUS_VERSIONS_INFO.labels(**labels).set(1)
|
43
76
|
|
44
77
|
|
45
|
-
def _read_versions() ->
|
78
|
+
def _read_versions() -> dict[str, dict[str, str]]:
|
46
79
|
"""Read the version."""
|
47
|
-
with open(
|
80
|
+
with open(_VERSIONS_PATH, encoding="utf-8") as file:
|
48
81
|
versions = json.load(file)
|
49
|
-
return cast(
|
82
|
+
return cast(dict[str, dict[str, str]], versions)
|
50
83
|
|
51
84
|
|
52
85
|
def get_version() -> Optional[str]:
|
53
86
|
"""Get the version."""
|
54
|
-
if not os.path.isfile(
|
87
|
+
if not os.path.isfile(_VERSIONS_PATH):
|
55
88
|
return None
|
56
89
|
versions = _read_versions()
|
57
90
|
return versions["main"]["git_hash"]
|
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015-2023, Camptocamp SA
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
8
|
+
list of conditions and the following disclaimer.
|
9
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
11
|
+
and/or other materials provided with the distribution.
|
12
|
+
|
13
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
14
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
15
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
16
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
17
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
18
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
19
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
20
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
21
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
22
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|