kinto 19.2.0__py3-none-any.whl → 19.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kinto might be problematic. Click here for more details.

Files changed (30) hide show
  1. kinto/__init__.py +1 -0
  2. kinto/core/__init__.py +1 -0
  3. kinto/core/initialization.py +59 -7
  4. kinto/core/metrics.py +37 -1
  5. kinto/core/resource/__init__.py +3 -2
  6. kinto/core/utils.py +8 -0
  7. kinto/plugins/admin/VERSION +1 -1
  8. kinto/plugins/admin/build/VERSION +1 -1
  9. kinto/plugins/admin/build/assets/index-BKIg2XW8.js +165 -0
  10. kinto/plugins/admin/build/assets/{index-vylaZGUr.css → index-D8oiN37x.css} +1 -1
  11. kinto/plugins/admin/build/assets/{javascript-upQ8KtFH.js → javascript-iSgyE4tI.js} +1 -1
  12. kinto/plugins/admin/build/index.html +2 -2
  13. kinto/plugins/prometheus.py +56 -20
  14. kinto/plugins/statsd.py +21 -1
  15. {kinto-19.2.0.dist-info → kinto-19.3.1.dist-info}/METADATA +29 -29
  16. {kinto-19.2.0.dist-info → kinto-19.3.1.dist-info}/RECORD +29 -29
  17. {kinto-19.2.0.dist-info → kinto-19.3.1.dist-info}/WHEEL +1 -1
  18. kinto/plugins/admin/build/assets/index-iVTdxamX.js +0 -175
  19. /kinto/plugins/admin/build/assets/{asn1-8gHclKtu.js → asn1-CGOzndHr.js} +0 -0
  20. /kinto/plugins/admin/build/assets/{clojure-plf_rynZ.js → clojure-BMjYHr_A.js} +0 -0
  21. /kinto/plugins/admin/build/assets/{css-tpsEXL3H.js → css-BnMrqG3P.js} +0 -0
  22. /kinto/plugins/admin/build/assets/{logo-FQUYikj1.png → logo-VBRiKSPX.png} +0 -0
  23. /kinto/plugins/admin/build/assets/{mllike-ilm95jrV.js → mllike-C_8OmSiT.js} +0 -0
  24. /kinto/plugins/admin/build/assets/{python-xljIYvii.js → python-BuPzkPfP.js} +0 -0
  25. /kinto/plugins/admin/build/assets/{rpm-cddeyEgF.js → rpm-CTu-6PCP.js} +0 -0
  26. /kinto/plugins/admin/build/assets/{sql-3IaSLchm.js → sql-C4g8LzGK.js} +0 -0
  27. /kinto/plugins/admin/build/assets/{ttcn-cfg-9oMIyPXS.js → ttcn-cfg-BIkV9KBc.js} +0 -0
  28. {kinto-19.2.0.dist-info → kinto-19.3.1.dist-info}/LICENSE +0 -0
  29. {kinto-19.2.0.dist-info → kinto-19.3.1.dist-info}/entry_points.txt +0 -0
  30. {kinto-19.2.0.dist-info → kinto-19.3.1.dist-info}/top_level.txt +0 -0
kinto/__init__.py CHANGED
@@ -38,6 +38,7 @@ DEFAULT_SETTINGS = {
38
38
  "record_id_generator": "kinto.views.RelaxedUUID",
39
39
  "project_name": "kinto",
40
40
  "admin_assets_path": None,
41
+ "metrics_matchdict_fields": ["bucket_id", "collection_id", "group_id", "record_id"],
41
42
  }
42
43
 
43
44
 
kinto/core/__init__.py CHANGED
@@ -96,6 +96,7 @@ DEFAULT_SETTINGS = {
96
96
  "statsd_backend": "kinto.core.statsd",
97
97
  "statsd_prefix": "kinto.core",
98
98
  "statsd_url": None,
99
+ "metrics_matchdict_fields": [],
99
100
  "storage_backend": "",
100
101
  "storage_url": "",
101
102
  "storage_max_fetch_size": 10000,
@@ -1,6 +1,7 @@
1
1
  import logging
2
2
  import random
3
3
  import re
4
+ import urllib.parse
4
5
  import warnings
5
6
  from datetime import datetime
6
7
  from secrets import token_hex
@@ -431,6 +432,9 @@ def setup_logging(config):
431
432
  def setup_metrics(config):
432
433
  settings = config.get_settings()
433
434
 
435
+ # Register a no-op metrics service by default.
436
+ config.registry.registerUtility(metrics.NoOpMetricsService(), metrics.IMetricsService)
437
+
434
438
  # This does not fully respect the Pyramid/ZCA patterns, but the rest of Kinto uses
435
439
  # `registry.storage`, `registry.cache`, etc. Consistency seems more important.
436
440
  config.registry.__class__.metrics = property(
@@ -449,9 +453,6 @@ def setup_metrics(config):
449
453
  def on_app_created(event):
450
454
  config = event.app
451
455
  metrics_service = config.registry.metrics
452
- if not metrics_service:
453
- logger.warning("No metrics service registered.")
454
- return
455
456
 
456
457
  metrics.watch_execution_time(metrics_service, config.registry.cache, prefix="backend")
457
458
  metrics.watch_execution_time(metrics_service, config.registry.storage, prefix="backend")
@@ -471,15 +472,66 @@ def setup_metrics(config):
471
472
  def on_new_response(event):
472
473
  request = event.request
473
474
  metrics_service = config.registry.metrics
474
- if not metrics_service:
475
- return
475
+
476
+ try:
477
+ endpoint = utils.strip_uri_prefix(request.path)
478
+ endpoint = urllib.parse.quote_plus(endpoint, safe="/?&=-_")
479
+ except UnicodeDecodeError as e:
480
+ # This `on_new_response` callback is also called when a HTTP 400
481
+ # is returned because of an invalid UTF-8 path. We still want metrics.
482
+ endpoint = str(e)
476
483
 
477
484
  # Count unique users.
478
485
  user_id = request.prefixed_userid
479
486
  if user_id:
480
487
  # Get rid of colons in metric packet (see #1282).
481
- user_id = user_id.replace(":", ".")
482
- metrics_service.count("users", unique=user_id)
488
+ auth, user_id = user_id.split(":")
489
+ metrics_service.count("users", unique=[("auth", auth), ("userid", user_id)])
490
+
491
+ # Add extra labels to metrics, based on fields extracted from the request matchdict.
492
+ metrics_matchdict_fields = aslist(settings["metrics_matchdict_fields"])
493
+ # Turn the `id` field of object endpoints into `{resource}_id` (eg. `mushroom_id`, `bucket_id`)
494
+ enhanced_matchdict = dict(request.matchdict or {})
495
+ try:
496
+ enhanced_matchdict[request.current_resource_name + "_id"] = enhanced_matchdict.get(
497
+ "id", ""
498
+ )
499
+ except AttributeError:
500
+ # Not on a resource.
501
+ pass
502
+ metrics_matchdict_labels = [
503
+ (field, enhanced_matchdict.get(field, "")) for field in metrics_matchdict_fields
504
+ ]
505
+
506
+ # Count served requests.
507
+ metrics_service.count(
508
+ "request_summary",
509
+ unique=[
510
+ ("method", request.method.lower()),
511
+ ("endpoint", endpoint),
512
+ ("status", str(event.response.status_code)),
513
+ ]
514
+ + metrics_matchdict_labels,
515
+ )
516
+
517
+ try:
518
+ current = utils.msec_time()
519
+ duration = current - request._received_at
520
+ metrics_service.observe(
521
+ "request_duration",
522
+ duration,
523
+ labels=[("endpoint", endpoint)] + metrics_matchdict_labels,
524
+ )
525
+ except AttributeError: # pragma: no cover
526
+ # Logging was not setup in this Kinto app (unlikely but possible)
527
+ pass
528
+
529
+ # Observe response size.
530
+ metrics_service.observe(
531
+ "request_size",
532
+ len(event.response.body or b""),
533
+ labels=[("endpoint", endpoint)] + metrics_matchdict_labels,
534
+ )
483
535
 
484
536
  # Count authentication verifications.
485
537
  if hasattr(request, "authn_type"):
kinto/core/metrics.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import types
2
2
 
3
- from zope.interface import Interface
3
+ from zope.interface import Interface, implementer
4
4
 
5
5
  from kinto.core import utils
6
6
 
@@ -16,13 +16,47 @@ class IMetricsService(Interface):
16
16
  Watch execution time.
17
17
  """
18
18
 
19
+ def observe(self, key, value, labels=[]):
20
+ """
21
+ Observe a give `value` for the specified `key`.
22
+ """
23
+
19
24
  def count(key, count=1, unique=None):
20
25
  """
21
26
  Count occurrences. If `unique` is set, overwrites the counter value
22
27
  on each call.
28
+
29
+ `unique` should be of type ``list[tuple[str,str]]``.
23
30
  """
24
31
 
25
32
 
33
+ class NoOpTimer:
34
+ def __call__(self, f):
35
+ @utils.safe_wraps(f)
36
+ def _wrapped(*args, **kwargs):
37
+ return f(*args, **kwargs)
38
+
39
+ return _wrapped
40
+
41
+ def __enter__(self):
42
+ pass
43
+
44
+ def __exit__(self, *args, **kwargs):
45
+ pass
46
+
47
+
48
+ @implementer(IMetricsService)
49
+ class NoOpMetricsService:
50
+ def timer(self, key):
51
+ return NoOpTimer()
52
+
53
+ def observe(self, key, value, labels=[]):
54
+ pass
55
+
56
+ def count(self, key, count=1, unique=None):
57
+ pass
58
+
59
+
26
60
  def watch_execution_time(metrics_service, obj, prefix="", classname=None):
27
61
  """
28
62
  Decorate all methods of an object in order to watch their execution time.
@@ -49,6 +83,8 @@ def listener_with_timer(config, key, func):
49
83
  def wrapped(*args, **kwargs):
50
84
  metrics_service = config.registry.metrics
51
85
  if not metrics_service:
86
+ # This only happens if `kinto.core.initialization.setup_metrics` is
87
+ # not listed in the `initialization_sequence` setting.
52
88
  return func(*args, **kwargs)
53
89
  # If metrics are enabled, monitor execution time of listeners.
54
90
  with metrics_service.timer(key):
@@ -665,7 +665,7 @@ class Resource:
665
665
  obj = self._get_object_or_404(self.object_id)
666
666
  self._raise_412_if_modified(obj)
667
667
 
668
- # Retreive the last_modified information from a querystring if present.
668
+ # Retrieve the last_modified information from a querystring if present.
669
669
  last_modified = self.request.validated["querystring"].get("last_modified")
670
670
 
671
671
  # If less or equal than current object. Ignore it.
@@ -1060,7 +1060,8 @@ class Resource:
1060
1060
  """Extracts filters from QueryString parameters."""
1061
1061
 
1062
1062
  def is_valid_timestamp(value):
1063
- return isinstance(value, int) or re.match(r'^"?\d+"?$', str(value))
1063
+ # Is either integer, or integer as string, or integer between 2 quotes.
1064
+ return isinstance(value, int) or re.match(r'^(\d+)$|^("\d+")$', str(value))
1064
1065
 
1065
1066
  queryparams = self.request.validated["querystring"]
1066
1067
 
kinto/core/utils.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import collections.abc as collections_abc
2
+ import functools
2
3
  import hashlib
3
4
  import hmac
4
5
  import os
@@ -541,3 +542,10 @@ def apply_json_patch(obj, ops):
541
542
  raise ValueError(e)
542
543
 
543
544
  return result
545
+
546
+
547
+ def safe_wraps(wrapper, *args, **kwargs):
548
+ """Safely wraps partial functions."""
549
+ while isinstance(wrapper, functools.partial):
550
+ wrapper = wrapper.func
551
+ return functools.wraps(wrapper, *args, **kwargs)
@@ -1 +1 @@
1
- 3.0.3
1
+ 3.4.1
@@ -1 +1 @@
1
- 3.0.3
1
+ 3.4.1