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.
Files changed (56) hide show
  1. c2cwsgiutils/__init__.py +12 -12
  2. c2cwsgiutils/acceptance/connection.py +5 -2
  3. c2cwsgiutils/acceptance/image.py +95 -3
  4. c2cwsgiutils/acceptance/package-lock.json +1933 -0
  5. c2cwsgiutils/acceptance/package.json +7 -0
  6. c2cwsgiutils/acceptance/print.py +3 -3
  7. c2cwsgiutils/acceptance/screenshot.js +62 -0
  8. c2cwsgiutils/acceptance/utils.py +14 -22
  9. c2cwsgiutils/auth.py +4 -4
  10. c2cwsgiutils/broadcast/__init__.py +15 -7
  11. c2cwsgiutils/broadcast/interface.py +3 -2
  12. c2cwsgiutils/broadcast/local.py +3 -2
  13. c2cwsgiutils/broadcast/redis.py +6 -5
  14. c2cwsgiutils/client_info.py +5 -5
  15. c2cwsgiutils/config_utils.py +2 -1
  16. c2cwsgiutils/db.py +20 -11
  17. c2cwsgiutils/db_maintenance_view.py +2 -1
  18. c2cwsgiutils/debug/_listeners.py +7 -6
  19. c2cwsgiutils/debug/_views.py +11 -10
  20. c2cwsgiutils/debug/utils.py +5 -5
  21. c2cwsgiutils/health_check.py +72 -73
  22. c2cwsgiutils/index.py +90 -105
  23. c2cwsgiutils/loader.py +3 -3
  24. c2cwsgiutils/logging_view.py +3 -2
  25. c2cwsgiutils/models_graph.py +4 -4
  26. c2cwsgiutils/prometheus.py +175 -57
  27. c2cwsgiutils/pyramid.py +4 -2
  28. c2cwsgiutils/pyramid_logging.py +2 -1
  29. c2cwsgiutils/redis_stats.py +13 -11
  30. c2cwsgiutils/redis_utils.py +11 -5
  31. c2cwsgiutils/request_tracking/__init__.py +36 -30
  32. c2cwsgiutils/scripts/genversion.py +4 -4
  33. c2cwsgiutils/scripts/stats_db.py +92 -60
  34. c2cwsgiutils/sentry.py +2 -1
  35. c2cwsgiutils/setup_process.py +12 -16
  36. c2cwsgiutils/sql_profiler/_impl.py +3 -2
  37. c2cwsgiutils/sqlalchemylogger/_models.py +2 -2
  38. c2cwsgiutils/sqlalchemylogger/handlers.py +6 -6
  39. c2cwsgiutils/static/favicon-16x16.png +0 -0
  40. c2cwsgiutils/static/favicon-32x32.png +0 -0
  41. c2cwsgiutils/stats_pyramid/__init__.py +7 -11
  42. c2cwsgiutils/stats_pyramid/_db_spy.py +14 -11
  43. c2cwsgiutils/stats_pyramid/_pyramid_spy.py +27 -21
  44. c2cwsgiutils/templates/index.html.mako +50 -0
  45. c2cwsgiutils/version.py +49 -16
  46. {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/METADATA +168 -99
  47. c2cwsgiutils-5.2.1.dev197.dist-info/RECORD +67 -0
  48. {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/WHEEL +1 -1
  49. c2cwsgiutils/acceptance/composition.py +0 -129
  50. c2cwsgiutils/metrics.py +0 -110
  51. c2cwsgiutils/scripts/check_es.py +0 -130
  52. c2cwsgiutils/stats.py +0 -344
  53. c2cwsgiutils/stats_pyramid/_views.py +0 -16
  54. c2cwsgiutils-5.2.1.dist-info/RECORD +0 -66
  55. {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/LICENSE +0 -0
  56. {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/entry_points.txt +0 -0
@@ -1,21 +1,29 @@
1
1
  #!/usr/bin/env python3
2
- """Emits statsd gauges for every tables of a database."""
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 Dict, List, Optional
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 stats
18
- from c2cwsgiutils.prometheus import PushgatewayGroupPublisher
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", type=str, action="append", help="A SQL query that returns a metric name and a value"
32
- )
33
- parser.add_argument(
34
- "--statsd-address", "--statsd_address", type=str, help="address:port for the statsd daemon"
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
- "--statsd-prefix", "--statsd_prefix", type=str, default="c2c", help="prefix for the statsd metrics"
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
- if args.statsd_address:
58
- self.statsd: Optional[stats.StatsDBackend] = stats.StatsDBackend(
59
- args.statsd_address, args.statsd_prefix, tags=stats.get_env_tags()
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
- else:
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: List[str], value: int, kind: str, tags: Optional[Dict[str, str]] = None
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
- if value > 0: # Don't export 0 values. We can always set null=0 in grafana...
79
- if self.statsd is not None:
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.prometheus is not None:
89
- self.prometheus.commit()
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: List[str], error_: Exception) -> None:
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: sqlalchemy.orm.scoped_session[sqlalchemy.orm.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: sqlalchemy.orm.scoped_session[sqlalchemy.orm.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="index_size",
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="index_usage",
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: sqlalchemy.orm.scoped_session[sqlalchemy.orm.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([schema, table], size, kind="size", tags={"schema": schema, "table": table})
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: sqlalchemy.orm.scoped_session[sqlalchemy.orm.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 replicatin lags. This estimate is updated on ANALYZE and VACUUM.
207
+ # and seems to cause replicating lags. This estimate is updated on ANALYZE and VACUUM.
192
208
  result = session.execute(
193
- sqlalchemy.text( # nosec
194
- "SELECT reltuples::bigint AS count FROM pg_class "
195
- f"WHERE oid = '{sqlalchemy.sql.quoted_name(schema, True)}."
196
- f"{sqlalchemy.sql.quoted_name(table, True)}'::regclass;"
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([schema, table], count, kind="count", tags={"schema": schema, "table": table})
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
- for metric, count in session.execute(sqlalchemy.text(extra)):
209
- reporter.do_report(str(metric).split("."), count, kind="count", tags={"metric": metric})
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.bootstrap_application_from_options(args)
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
- continue
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 typing import Any, Callable, Generator, MutableMapping, Optional
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
@@ -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, Dict, Optional, TypedDict, cast
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, stats
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 = TypedDict(
67
- "PyramidEnv",
68
- {
69
- "root": Any,
70
- "closer": Callable[..., Any],
71
- "registry": pyramid.registry.Registry,
72
- "request": pyramid.request.Request,
73
- "root_factory": object,
74
- "app": Callable[[Dict[str, str], Any], Any],
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[Dict[str, Any]] = None,
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, Mapping, Set
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: Set[str] = set()
26
+ self._repo: set[str] = set()
26
27
 
27
28
  def profile(
28
29
  self,
@@ -1,4 +1,4 @@
1
- from typing import Any, Dict, Union
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, Dict[str, str]] = "") -> Any:
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, Dict, List
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: Dict[str, str],
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.monotonic()
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.monotonic() >= (time_since_last + self.MAX_TIMEOUT)
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: List[Any]) -> None:
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 (SQLAlchemyError):
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 stats
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
- _views.init(config)
30
- from . import _pyramid_spy
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
- from typing import Any, Callable, Dict, Optional
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 stats
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
- if stats.USE_TAGS and what != "commit":
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
- duration = measure.stop()
71
- LOG.debug("Execute statement '%s' in %d.", what, duration)
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
- from typing import Any, Callable, Dict, Optional
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 stats
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: stats.Timer
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 = 500
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
- if stats.USE_TAGS:
48
- key = [kind]
49
- tags: Optional[Dict[str, Any]] = {
50
- "method": request.method,
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
- measure = stats.timer()
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
- measure = stats.timer()
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>