c2cwsgiutils 5.2.1__py3-none-any.whl → 5.2.1.dev197__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- c2cwsgiutils/__init__.py +12 -12
- c2cwsgiutils/acceptance/connection.py +5 -2
- c2cwsgiutils/acceptance/image.py +95 -3
- c2cwsgiutils/acceptance/package-lock.json +1933 -0
- c2cwsgiutils/acceptance/package.json +7 -0
- c2cwsgiutils/acceptance/print.py +3 -3
- c2cwsgiutils/acceptance/screenshot.js +62 -0
- c2cwsgiutils/acceptance/utils.py +14 -22
- c2cwsgiutils/auth.py +4 -4
- c2cwsgiutils/broadcast/__init__.py +15 -7
- c2cwsgiutils/broadcast/interface.py +3 -2
- c2cwsgiutils/broadcast/local.py +3 -2
- c2cwsgiutils/broadcast/redis.py +6 -5
- c2cwsgiutils/client_info.py +5 -5
- c2cwsgiutils/config_utils.py +2 -1
- c2cwsgiutils/db.py +20 -11
- c2cwsgiutils/db_maintenance_view.py +2 -1
- c2cwsgiutils/debug/_listeners.py +7 -6
- c2cwsgiutils/debug/_views.py +11 -10
- c2cwsgiutils/debug/utils.py +5 -5
- c2cwsgiutils/health_check.py +72 -73
- c2cwsgiutils/index.py +90 -105
- c2cwsgiutils/loader.py +3 -3
- c2cwsgiutils/logging_view.py +3 -2
- c2cwsgiutils/models_graph.py +4 -4
- c2cwsgiutils/prometheus.py +175 -57
- c2cwsgiutils/pyramid.py +4 -2
- c2cwsgiutils/pyramid_logging.py +2 -1
- c2cwsgiutils/redis_stats.py +13 -11
- c2cwsgiutils/redis_utils.py +11 -5
- c2cwsgiutils/request_tracking/__init__.py +36 -30
- c2cwsgiutils/scripts/genversion.py +4 -4
- c2cwsgiutils/scripts/stats_db.py +92 -60
- c2cwsgiutils/sentry.py +2 -1
- c2cwsgiutils/setup_process.py +12 -16
- c2cwsgiutils/sql_profiler/_impl.py +3 -2
- c2cwsgiutils/sqlalchemylogger/_models.py +2 -2
- c2cwsgiutils/sqlalchemylogger/handlers.py +6 -6
- c2cwsgiutils/static/favicon-16x16.png +0 -0
- c2cwsgiutils/static/favicon-32x32.png +0 -0
- c2cwsgiutils/stats_pyramid/__init__.py +7 -11
- c2cwsgiutils/stats_pyramid/_db_spy.py +14 -11
- c2cwsgiutils/stats_pyramid/_pyramid_spy.py +27 -21
- c2cwsgiutils/templates/index.html.mako +50 -0
- c2cwsgiutils/version.py +49 -16
- {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/METADATA +168 -99
- c2cwsgiutils-5.2.1.dev197.dist-info/RECORD +67 -0
- {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/WHEEL +1 -1
- c2cwsgiutils/acceptance/composition.py +0 -129
- c2cwsgiutils/metrics.py +0 -110
- c2cwsgiutils/scripts/check_es.py +0 -130
- c2cwsgiutils/stats.py +0 -344
- c2cwsgiutils/stats_pyramid/_views.py +0 -16
- c2cwsgiutils-5.2.1.dist-info/RECORD +0 -66
- {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/LICENSE +0 -0
- {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/entry_points.txt +0 -0
c2cwsgiutils/scripts/stats_db.py
CHANGED
@@ -1,21 +1,29 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
|
-
"""
|
2
|
+
"""Provide prometheus gauges for every tables of a database."""
|
3
|
+
|
3
4
|
import argparse
|
4
5
|
import logging
|
5
6
|
import os
|
6
7
|
import sys
|
7
8
|
import time
|
8
|
-
from typing import
|
9
|
+
from typing import TYPE_CHECKING, Optional
|
10
|
+
from wsgiref.simple_server import make_server
|
9
11
|
|
10
12
|
import sqlalchemy
|
11
13
|
import sqlalchemy.exc
|
12
14
|
import sqlalchemy.orm
|
13
15
|
import transaction
|
16
|
+
from prometheus_client import CollectorRegistry, Gauge, push_to_gateway
|
17
|
+
from prometheus_client.exposition import make_wsgi_app
|
14
18
|
from zope.sqlalchemy import register
|
15
19
|
|
16
20
|
import c2cwsgiutils.setup_process
|
17
|
-
from c2cwsgiutils import
|
18
|
-
|
21
|
+
from c2cwsgiutils import prometheus
|
22
|
+
|
23
|
+
if TYPE_CHECKING:
|
24
|
+
scoped_session = sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]
|
25
|
+
else:
|
26
|
+
scoped_session = sqlalchemy.orm.scoped_session
|
19
27
|
|
20
28
|
LOG = logging.getLogger(__name__)
|
21
29
|
|
@@ -28,13 +36,17 @@ def _parse_args() -> argparse.Namespace:
|
|
28
36
|
"--schema", type=str, action="append", required=True, default=["public"], help="schema to dump"
|
29
37
|
)
|
30
38
|
parser.add_argument(
|
31
|
-
"--extra",
|
32
|
-
|
33
|
-
|
34
|
-
"
|
39
|
+
"--extra",
|
40
|
+
type=str,
|
41
|
+
action="append",
|
42
|
+
help="A SQL query that returns a metric name and a value",
|
35
43
|
)
|
36
44
|
parser.add_argument(
|
37
|
-
"--
|
45
|
+
"--extra-gauge",
|
46
|
+
type=str,
|
47
|
+
action="append",
|
48
|
+
nargs=3,
|
49
|
+
help="A SQL query that returns a metric name and a value, with gauge name and help",
|
38
50
|
)
|
39
51
|
parser.add_argument(
|
40
52
|
"--prometheus-url", "--prometheus_url", type=str, help="Base URL for the Prometheus Pushgateway"
|
@@ -54,43 +66,39 @@ class Reporter:
|
|
54
66
|
|
55
67
|
def __init__(self, args: argparse.Namespace) -> None:
|
56
68
|
self._error: Optional[Exception] = None
|
57
|
-
|
58
|
-
|
59
|
-
|
69
|
+
self.registry = CollectorRegistry()
|
70
|
+
self.prometheus_push = args.prometheus_url is not None
|
71
|
+
self.args = args
|
72
|
+
self.gauges: dict[str, Gauge] = {}
|
73
|
+
|
74
|
+
def get_gauge(self, kind: str, kind_help: str, labels: list[str]) -> Gauge:
|
75
|
+
if kind not in self.gauges:
|
76
|
+
self.gauges[kind] = Gauge(
|
77
|
+
prometheus.build_metric_name(f"database_{kind}"),
|
78
|
+
kind_help,
|
79
|
+
labels,
|
80
|
+
registry=self.registry,
|
60
81
|
)
|
61
|
-
|
62
|
-
self.statsd = None
|
63
|
-
|
64
|
-
if args.prometheus_url:
|
65
|
-
self.prometheus: Optional[PushgatewayGroupPublisher] = PushgatewayGroupPublisher(
|
66
|
-
args.prometheus_url,
|
67
|
-
"db_counts",
|
68
|
-
instance=args.prometheus_instance,
|
69
|
-
labels=stats.get_env_tags(),
|
70
|
-
)
|
71
|
-
else:
|
72
|
-
self.prometheus = None
|
82
|
+
return self.gauges[kind]
|
73
83
|
|
74
84
|
def do_report(
|
75
|
-
self, metric:
|
85
|
+
self, metric: list[str], value: int, kind: str, kind_help: str, tags: dict[str, str]
|
76
86
|
) -> None:
|
77
87
|
LOG.debug("%s.%s -> %d", kind, ".".join(metric), value)
|
78
|
-
|
79
|
-
|
80
|
-
if stats.USE_TAGS and tags is not None:
|
81
|
-
self.statsd.gauge([kind], value, tags=tags)
|
82
|
-
else:
|
83
|
-
self.statsd.gauge([kind] + metric, value)
|
84
|
-
if self.prometheus is not None:
|
85
|
-
self.prometheus.add("database_table_" + kind, value, metric_labels=tags)
|
88
|
+
gauge = self.get_gauge(kind, kind_help, list(tags.keys()))
|
89
|
+
gauge.labels(**tags).set(value)
|
86
90
|
|
87
91
|
def commit(self) -> None:
|
88
|
-
if self.
|
89
|
-
self.
|
92
|
+
if self.prometheus_push:
|
93
|
+
push_to_gateway(self.args.prometheus_url, job="db_counts", registry=self.registry)
|
94
|
+
else:
|
95
|
+
port = int(os.environ.get("C2C_PROMETHEUS_PORT", "9090"))
|
96
|
+
app = make_wsgi_app(self.registry)
|
97
|
+
with make_server("", port, app) as httpd:
|
98
|
+
LOG.info("Waiting that Prometheus get the metrics served on port %s...", port)
|
99
|
+
httpd.handle_request()
|
90
100
|
|
91
|
-
def error(self, metric:
|
92
|
-
if self.statsd is not None:
|
93
|
-
self.statsd.counter(["error"] + metric, 1)
|
101
|
+
def error(self, metric: list[str], error_: Exception) -> None:
|
94
102
|
if self._error is None:
|
95
103
|
self._error = error_
|
96
104
|
|
@@ -100,7 +108,7 @@ class Reporter:
|
|
100
108
|
|
101
109
|
|
102
110
|
def do_table(
|
103
|
-
session:
|
111
|
+
session: scoped_session,
|
104
112
|
schema: str,
|
105
113
|
table: str,
|
106
114
|
reporter: Reporter,
|
@@ -114,7 +122,7 @@ def do_table(
|
|
114
122
|
def _do_indexes(
|
115
123
|
reporter: Reporter,
|
116
124
|
schema: str,
|
117
|
-
session:
|
125
|
+
session: scoped_session,
|
118
126
|
table: str,
|
119
127
|
) -> None:
|
120
128
|
for index_name, size_main, size_fsm, number_of_scans, tuples_read, tuples_fetched in session.execute(
|
@@ -146,14 +154,16 @@ def _do_indexes(
|
|
146
154
|
reporter.do_report(
|
147
155
|
[schema, table, index_name, fork],
|
148
156
|
value,
|
149
|
-
kind="
|
157
|
+
kind="table_index_size",
|
158
|
+
kind_help="Size of the index",
|
150
159
|
tags={"schema": schema, "table": table, "index": index_name, "fork": fork},
|
151
160
|
)
|
152
161
|
for action, value in (("scan", number_of_scans), ("read", tuples_read), ("fetch", tuples_fetched)):
|
153
162
|
reporter.do_report(
|
154
163
|
[schema, table, index_name, action],
|
155
164
|
value,
|
156
|
-
kind="
|
165
|
+
kind="table_index_usage",
|
166
|
+
kind_help="Usage of the index",
|
157
167
|
tags={"schema": schema, "table": table, "index": index_name, "action": action},
|
158
168
|
)
|
159
169
|
|
@@ -161,7 +171,7 @@ def _do_indexes(
|
|
161
171
|
def _do_table_size(
|
162
172
|
reporter: Reporter,
|
163
173
|
schema: str,
|
164
|
-
session:
|
174
|
+
session: scoped_session,
|
165
175
|
table: str,
|
166
176
|
) -> None:
|
167
177
|
result = session.execute(
|
@@ -178,35 +188,48 @@ def _do_table_size(
|
|
178
188
|
assert result is not None
|
179
189
|
size: int
|
180
190
|
(size,) = result
|
181
|
-
reporter.do_report(
|
191
|
+
reporter.do_report(
|
192
|
+
[schema, table],
|
193
|
+
size,
|
194
|
+
kind="table_size",
|
195
|
+
kind_help="Size of the table",
|
196
|
+
tags={"schema": schema, "table": table},
|
197
|
+
)
|
182
198
|
|
183
199
|
|
184
200
|
def _do_table_count(
|
185
201
|
reporter: Reporter,
|
186
202
|
schema: str,
|
187
|
-
session:
|
203
|
+
session: scoped_session,
|
188
204
|
table: str,
|
189
205
|
) -> None:
|
190
206
|
# We request and estimation of the count as a real count is very slow on big tables
|
191
|
-
# and seems to cause
|
207
|
+
# and seems to cause replicating lags. This estimate is updated on ANALYZE and VACUUM.
|
192
208
|
result = session.execute(
|
193
|
-
sqlalchemy.text(
|
194
|
-
"SELECT reltuples
|
195
|
-
|
196
|
-
|
197
|
-
|
209
|
+
sqlalchemy.text(
|
210
|
+
"SELECT reltuples FROM pg_class where "
|
211
|
+
"oid=(quote_ident(:schema) || '.' || quote_ident(:table))::regclass;"
|
212
|
+
),
|
213
|
+
params={"schema": schema, "table": table},
|
198
214
|
).fetchone()
|
199
215
|
assert result is not None
|
200
216
|
(count,) = result
|
201
|
-
reporter.do_report(
|
217
|
+
reporter.do_report(
|
218
|
+
[schema, table],
|
219
|
+
count,
|
220
|
+
kind="table_count",
|
221
|
+
kind_help="The number of row in the table",
|
222
|
+
tags={"schema": schema, "table": table},
|
223
|
+
)
|
202
224
|
|
203
225
|
|
204
|
-
def do_extra(
|
205
|
-
session: sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session], extra: str, reporter: Reporter
|
206
|
-
) -> None:
|
226
|
+
def do_extra(session: scoped_session, sql: str, kind: str, gauge_help: str, reporter: Reporter) -> None:
|
207
227
|
"""Do an extra report."""
|
208
|
-
|
209
|
-
|
228
|
+
|
229
|
+
for metric, count in session.execute(sqlalchemy.text(sql)):
|
230
|
+
reporter.do_report(
|
231
|
+
str(metric).split("."), count, kind=kind, kind_help=gauge_help, tags={"metric": metric}
|
232
|
+
)
|
210
233
|
|
211
234
|
|
212
235
|
def _do_dtats_db(args: argparse.Namespace) -> None:
|
@@ -241,10 +264,19 @@ def _do_dtats_db(args: argparse.Namespace) -> None:
|
|
241
264
|
for pos, extra in enumerate(args.extra):
|
242
265
|
LOG.info("Process extra %s.", extra)
|
243
266
|
try:
|
244
|
-
do_extra(session, extra, reporter)
|
267
|
+
do_extra(session, extra, "extra", "Extra metric", reporter)
|
245
268
|
except Exception as e: # pylint: disable=broad-except
|
246
269
|
LOG.exception("Process extra %s error.", extra)
|
247
270
|
reporter.error(["extra", str(pos + 1)], e)
|
271
|
+
if args.extra_gauge:
|
272
|
+
for pos, extra in enumerate(args.extra_gauge):
|
273
|
+
sql, gauge, gauge_help = extra
|
274
|
+
LOG.info("Process extra %s.", extra)
|
275
|
+
try:
|
276
|
+
do_extra(session, sql, gauge, gauge_help, reporter)
|
277
|
+
except Exception as e: # pylint: disable=broad-except
|
278
|
+
LOG.exception("Process extra %s error.", extra)
|
279
|
+
reporter.error(["extra", str(len(args.extra) + pos + 1)], e)
|
248
280
|
|
249
281
|
reporter.commit()
|
250
282
|
transaction.abort()
|
@@ -255,12 +287,12 @@ def main() -> None:
|
|
255
287
|
"""Run the command."""
|
256
288
|
success = False
|
257
289
|
args = _parse_args()
|
258
|
-
c2cwsgiutils.setup_process.
|
290
|
+
c2cwsgiutils.setup_process.init(args.config_uri)
|
259
291
|
for _ in range(int(os.environ.get("C2CWSGIUTILS_STATS_DB_TRYNUMBER", 10))):
|
260
292
|
try:
|
261
293
|
_do_dtats_db(args)
|
262
294
|
success = True
|
263
|
-
|
295
|
+
break
|
264
296
|
except: # pylint: disable=bare-except
|
265
297
|
LOG.exception("Exception during run")
|
266
298
|
time.sleep(float(os.environ.get("C2CWSGIUTILS_STATS_DB_SLEEP", 1)))
|
c2cwsgiutils/sentry.py
CHANGED
@@ -2,7 +2,8 @@ import contextlib
|
|
2
2
|
import logging
|
3
3
|
import os
|
4
4
|
import warnings
|
5
|
-
from
|
5
|
+
from collections.abc import Generator, MutableMapping
|
6
|
+
from typing import Any, Callable, Optional
|
6
7
|
|
7
8
|
import pyramid.config
|
8
9
|
import sentry_sdk
|
c2cwsgiutils/setup_process.py
CHANGED
@@ -7,7 +7,7 @@ Must be imported at the very beginning of the process's life, before any other m
|
|
7
7
|
|
8
8
|
import argparse
|
9
9
|
import warnings
|
10
|
-
from typing import Any, Callable,
|
10
|
+
from typing import Any, Callable, Optional, TypedDict, cast
|
11
11
|
|
12
12
|
import pyramid.config
|
13
13
|
import pyramid.registry
|
@@ -16,7 +16,7 @@ import pyramid.router
|
|
16
16
|
from pyramid.paster import bootstrap
|
17
17
|
from pyramid.scripts.common import get_config_loader, parse_vars
|
18
18
|
|
19
|
-
from c2cwsgiutils import broadcast, coverage_setup, redis_stats, sentry, sql_profiler
|
19
|
+
from c2cwsgiutils import broadcast, coverage_setup, redis_stats, sentry, sql_profiler
|
20
20
|
|
21
21
|
|
22
22
|
def fill_arguments(
|
@@ -51,7 +51,6 @@ def init(config_file: str = "c2c:///app/production.ini") -> None:
|
|
51
51
|
coverage_setup.includeme()
|
52
52
|
sentry.includeme(config)
|
53
53
|
broadcast.includeme(config)
|
54
|
-
stats.init_backends(settings)
|
55
54
|
redis_stats.includeme(config)
|
56
55
|
sql_profiler.includeme(config)
|
57
56
|
|
@@ -63,18 +62,15 @@ def init_logging(config_file: str = "c2c:///app/production.ini") -> None:
|
|
63
62
|
loader.setup_logging(None)
|
64
63
|
|
65
64
|
|
66
|
-
PyramidEnv =
|
67
|
-
"
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
},
|
76
|
-
total=True,
|
77
|
-
)
|
65
|
+
class PyramidEnv(TypedDict, total=True):
|
66
|
+
"""The return type of the bootstrap functions."""
|
67
|
+
|
68
|
+
root: Any
|
69
|
+
closer: Callable[..., Any]
|
70
|
+
registry: pyramid.registry.Registry
|
71
|
+
request: pyramid.request.Request
|
72
|
+
root_factory: object
|
73
|
+
app: Callable[[dict[str, str], Any], Any]
|
78
74
|
|
79
75
|
|
80
76
|
def bootstrap_application_from_options(options: argparse.Namespace) -> PyramidEnv:
|
@@ -91,7 +87,7 @@ def bootstrap_application_from_options(options: argparse.Namespace) -> PyramidEn
|
|
91
87
|
|
92
88
|
def bootstrap_application(
|
93
89
|
config_uri: str = "c2c:///app/production.ini",
|
94
|
-
options: Optional[
|
90
|
+
options: Optional[dict[str, Any]] = None,
|
95
91
|
) -> PyramidEnv:
|
96
92
|
"""
|
97
93
|
Initialize all the application.
|
@@ -5,8 +5,9 @@ That runs an "EXPLAIN ANALYZE" on every SELECT query going through SQLAlchemy.
|
|
5
5
|
"""
|
6
6
|
import logging
|
7
7
|
import re
|
8
|
+
from collections.abc import Mapping
|
8
9
|
from threading import Lock
|
9
|
-
from typing import Any
|
10
|
+
from typing import Any
|
10
11
|
|
11
12
|
import pyramid.request
|
12
13
|
import sqlalchemy.engine
|
@@ -22,7 +23,7 @@ class _Repository:
|
|
22
23
|
def __init__(self) -> None:
|
23
24
|
super().__init__()
|
24
25
|
self._lock = Lock()
|
25
|
-
self._repo:
|
26
|
+
self._repo: set[str] = set()
|
26
27
|
|
27
28
|
def profile(
|
28
29
|
self,
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from typing import Any,
|
1
|
+
from typing import Any, Union
|
2
2
|
|
3
3
|
from sqlalchemy import Column
|
4
4
|
from sqlalchemy.orm import declarative_base
|
@@ -8,7 +8,7 @@ from sqlalchemy.types import DateTime, Integer, String
|
|
8
8
|
Base = declarative_base()
|
9
9
|
|
10
10
|
|
11
|
-
def create_log_class(tablename: str = "logs", tableargs: Union[str,
|
11
|
+
def create_log_class(tablename: str = "logs", tableargs: Union[str, dict[str, str]] = "") -> Any:
|
12
12
|
"""Get the sqlalchemy lgo class."""
|
13
13
|
|
14
14
|
class Log(Base): # type: ignore
|
@@ -3,7 +3,7 @@ import queue
|
|
3
3
|
import threading
|
4
4
|
import time
|
5
5
|
import traceback
|
6
|
-
from typing import Any
|
6
|
+
from typing import Any
|
7
7
|
|
8
8
|
import sqlalchemy
|
9
9
|
from sqlalchemy import create_engine
|
@@ -25,7 +25,7 @@ class SQLAlchemyHandler(logging.Handler):
|
|
25
25
|
|
26
26
|
def __init__(
|
27
27
|
self,
|
28
|
-
sqlalchemy_url:
|
28
|
+
sqlalchemy_url: dict[str, str],
|
29
29
|
does_not_contain_expression: str = "",
|
30
30
|
contains_expression: str = "",
|
31
31
|
) -> None:
|
@@ -54,7 +54,7 @@ class SQLAlchemyHandler(logging.Handler):
|
|
54
54
|
LOG.debug("%s: starting processor thread", __name__)
|
55
55
|
while True:
|
56
56
|
logs = []
|
57
|
-
time_since_last = time.
|
57
|
+
time_since_last = time.perf_counter()
|
58
58
|
while True:
|
59
59
|
with self.condition:
|
60
60
|
self.condition.wait(timeout=self.MAX_TIMEOUT)
|
@@ -66,17 +66,17 @@ class SQLAlchemyHandler(logging.Handler):
|
|
66
66
|
# by writing chunks of self.MAX_NB_LOGS size,
|
67
67
|
# but also do not wait forever before writing stuff (self.MAX_TIMOUT)
|
68
68
|
if (len(logs) >= self.MAX_NB_LOGS) or (
|
69
|
-
time.
|
69
|
+
time.perf_counter() >= (time_since_last + self.MAX_TIMEOUT)
|
70
70
|
):
|
71
71
|
self._write_logs(logs)
|
72
72
|
break
|
73
73
|
LOG.debug("%s: stopping processor thread", __name__)
|
74
74
|
|
75
|
-
def _write_logs(self, logs:
|
75
|
+
def _write_logs(self, logs: list[Any]) -> None:
|
76
76
|
try:
|
77
77
|
self.session.bulk_save_objects(logs)
|
78
78
|
self.session.commit()
|
79
|
-
except
|
79
|
+
except SQLAlchemyError:
|
80
80
|
try:
|
81
81
|
self.create_db()
|
82
82
|
self.session.rollback()
|
Binary file
|
Binary file
|
@@ -1,40 +1,36 @@
|
|
1
1
|
"""Generate statsd metrics for pyramid and SQLAlchemy events."""
|
2
|
+
|
2
3
|
import warnings
|
3
4
|
|
4
5
|
import pyramid.config
|
5
6
|
import pyramid.request
|
6
7
|
|
7
|
-
from c2cwsgiutils import
|
8
|
+
from c2cwsgiutils.stats_pyramid import _pyramid_spy
|
8
9
|
|
9
10
|
|
10
11
|
def init(config: pyramid.config.Configurator) -> None:
|
11
12
|
"""Initialize the whole stats module, for backward compatibility."""
|
13
|
+
|
12
14
|
warnings.warn("init function is deprecated; use includeme instead")
|
13
15
|
includeme(config)
|
14
16
|
|
15
17
|
|
16
18
|
def includeme(config: pyramid.config.Configurator) -> None:
|
17
19
|
"""
|
18
|
-
Initialize the whole stats module.
|
20
|
+
Initialize the whole stats pyramid module.
|
19
21
|
|
20
22
|
Arguments:
|
21
23
|
|
22
24
|
config: The Pyramid config
|
23
25
|
"""
|
24
|
-
stats.init_backends(config.get_settings())
|
25
|
-
if stats.BACKENDS: # pragma: nocover
|
26
|
-
if "memory" in stats.BACKENDS: # pragma: nocover
|
27
|
-
from . import _views
|
28
26
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
_pyramid_spy.init(config)
|
33
|
-
init_db_spy()
|
27
|
+
_pyramid_spy.init(config)
|
28
|
+
init_db_spy()
|
34
29
|
|
35
30
|
|
36
31
|
def init_db_spy() -> None:
|
37
32
|
"""Initialize the database spy."""
|
33
|
+
|
38
34
|
from . import _db_spy
|
39
35
|
|
40
36
|
_db_spy.init()
|
@@ -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]] = {"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(
|
@@ -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,35 +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
|
-
"route": name,
|
52
|
-
"status": status,
|
53
|
-
"group": status // 100,
|
54
|
-
}
|
55
|
-
else:
|
56
|
-
key = [kind, request.method, name, status]
|
57
|
-
tags = None
|
58
|
-
duration = measure.stop(key, tags)
|
59
|
-
_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)
|
60
68
|
|
61
69
|
return finished_cb
|
62
70
|
|
63
71
|
|
64
72
|
def _request_callback(event: pyramid.events.NewRequest) -> None: # pragma: nocover
|
65
73
|
"""Finish the callback called when a new HTTP request is incoming."""
|
66
|
-
|
67
|
-
event.request.add_finished_callback(_create_finished_cb("route", measure))
|
74
|
+
event.request.add_finished_callback(_create_finished_cb("route", _PROMETHEUS_PYRAMID_ROUTES_SUMMARY))
|
68
75
|
|
69
76
|
|
70
77
|
def _before_rendered_callback(event: pyramid.events.BeforeRender) -> None: # pragma: nocover
|
71
78
|
"""Finish the callback called when the rendering is starting."""
|
72
79
|
request = event.get("request", None)
|
73
80
|
if request:
|
74
|
-
|
75
|
-
request.add_finished_callback(_create_finished_cb("render", measure))
|
81
|
+
request.add_finished_callback(_create_finished_cb("render", _PROMETHEUS_PYRAMID_VIEWS_SUMMARY))
|
76
82
|
|
77
83
|
|
78
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>
|