kinto 18.1.1__py3-none-any.whl → 19.3.0__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 (33) hide show
  1. kinto/__init__.py +1 -0
  2. kinto/core/__init__.py +2 -1
  3. kinto/core/initialization.py +132 -53
  4. kinto/core/metrics.py +93 -0
  5. kinto/core/statsd.py +1 -63
  6. kinto/core/testing.py +5 -1
  7. kinto/core/utils.py +8 -0
  8. kinto/plugins/admin/VERSION +1 -1
  9. kinto/plugins/admin/build/VERSION +1 -1
  10. kinto/plugins/admin/build/assets/index-BKIg2XW8.js +165 -0
  11. kinto/plugins/admin/build/assets/{index-vylaZGUr.css → index-D8oiN37x.css} +1 -1
  12. kinto/plugins/admin/build/assets/{javascript-upQ8KtFH.js → javascript-iSgyE4tI.js} +1 -1
  13. kinto/plugins/admin/build/index.html +2 -2
  14. kinto/plugins/history/__init__.py +5 -6
  15. kinto/plugins/prometheus.py +186 -0
  16. kinto/plugins/quotas/__init__.py +5 -6
  17. kinto/plugins/statsd.py +70 -0
  18. {kinto-18.1.1.dist-info → kinto-19.3.0.dist-info}/METADATA +5 -4
  19. {kinto-18.1.1.dist-info → kinto-19.3.0.dist-info}/RECORD +32 -29
  20. {kinto-18.1.1.dist-info → kinto-19.3.0.dist-info}/WHEEL +1 -1
  21. kinto/plugins/admin/build/assets/index-iVTdxamX.js +0 -175
  22. /kinto/plugins/admin/build/assets/{asn1-8gHclKtu.js → asn1-CGOzndHr.js} +0 -0
  23. /kinto/plugins/admin/build/assets/{clojure-plf_rynZ.js → clojure-BMjYHr_A.js} +0 -0
  24. /kinto/plugins/admin/build/assets/{css-tpsEXL3H.js → css-BnMrqG3P.js} +0 -0
  25. /kinto/plugins/admin/build/assets/{logo-FQUYikj1.png → logo-VBRiKSPX.png} +0 -0
  26. /kinto/plugins/admin/build/assets/{mllike-ilm95jrV.js → mllike-C_8OmSiT.js} +0 -0
  27. /kinto/plugins/admin/build/assets/{python-xljIYvii.js → python-BuPzkPfP.js} +0 -0
  28. /kinto/plugins/admin/build/assets/{rpm-cddeyEgF.js → rpm-CTu-6PCP.js} +0 -0
  29. /kinto/plugins/admin/build/assets/{sql-3IaSLchm.js → sql-C4g8LzGK.js} +0 -0
  30. /kinto/plugins/admin/build/assets/{ttcn-cfg-9oMIyPXS.js → ttcn-cfg-BIkV9KBc.js} +0 -0
  31. {kinto-18.1.1.dist-info → kinto-19.3.0.dist-info}/LICENSE +0 -0
  32. {kinto-18.1.1.dist-info → kinto-19.3.0.dist-info}/entry_points.txt +0 -0
  33. {kinto-18.1.1.dist-info → kinto-19.3.0.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
@@ -66,8 +66,8 @@ DEFAULT_SETTINGS = {
66
66
  "kinto.core.initialization.setup_authentication",
67
67
  "kinto.core.initialization.setup_backoff",
68
68
  "kinto.core.initialization.setup_sentry",
69
- "kinto.core.initialization.setup_statsd",
70
69
  "kinto.core.initialization.setup_listeners",
70
+ "kinto.core.initialization.setup_metrics",
71
71
  "kinto.core.events.setup_transaction_hook",
72
72
  ),
73
73
  "event_listeners": "",
@@ -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,
@@ -21,7 +21,7 @@ from pyramid.security import NO_PERMISSION_REQUIRED
21
21
  from pyramid.settings import asbool, aslist
22
22
  from pyramid_multiauth import MultiAuthenticationPolicy, MultiAuthPolicySelected
23
23
 
24
- from kinto.core import cache, errors, permission, storage, utils
24
+ from kinto.core import cache, errors, metrics, permission, storage, utils
25
25
  from kinto.core.events import ACTIONS, ResourceChanged, ResourceRead
26
26
 
27
27
 
@@ -334,51 +334,13 @@ def setup_sentry(config):
334
334
 
335
335
 
336
336
  def setup_statsd(config):
337
- settings = config.get_settings()
338
- config.registry.statsd = None
339
-
340
- if settings["statsd_url"]:
341
- statsd_mod = settings["statsd_backend"]
342
- statsd_mod = config.maybe_dotted(statsd_mod)
343
- client = statsd_mod.load_from_config(config)
344
-
345
- config.registry.statsd = client
346
-
347
- client.watch_execution_time(config.registry.cache, prefix="backend")
348
- client.watch_execution_time(config.registry.storage, prefix="backend")
349
- client.watch_execution_time(config.registry.permission, prefix="backend")
350
-
351
- # Commit so that configured policy can be queried.
352
- config.commit()
353
- policy = config.registry.queryUtility(IAuthenticationPolicy)
354
- if isinstance(policy, MultiAuthenticationPolicy):
355
- for name, subpolicy in policy.get_policies():
356
- client.watch_execution_time(subpolicy, prefix="authentication", classname=name)
357
- else:
358
- client.watch_execution_time(policy, prefix="authentication")
359
-
360
- def on_new_response(event):
361
- request = event.request
362
-
363
- # Count unique users.
364
- user_id = request.prefixed_userid
365
- if user_id:
366
- # Get rid of colons in metric packet (see #1282).
367
- user_id = user_id.replace(":", ".")
368
- client.count("users", unique=user_id)
369
-
370
- # Count authentication verifications.
371
- if hasattr(request, "authn_type"):
372
- client.count(f"authn_type.{request.authn_type}")
373
-
374
- # Count view calls.
375
- service = request.current_service
376
- if service:
377
- client.count(f"view.{service.name}.{request.method}")
378
-
379
- config.add_subscriber(on_new_response, NewResponse)
380
-
381
- return client
337
+ # It would be pretty rare to find users that have a custom ``kinto.initialization_sequence`` setting.
338
+ # But just in case, warn that it will be removed in next major.
339
+ warnings.warn(
340
+ "``setup_statsd()`` is now deprecated. Use ``kinto.core.initialization.setup_metrics()`` instead.",
341
+ DeprecationWarning,
342
+ )
343
+ setup_metrics(config)
382
344
 
383
345
 
384
346
  def install_middlewares(app, settings):
@@ -466,6 +428,125 @@ def setup_logging(config):
466
428
  config.add_subscriber(on_new_response, NewResponse)
467
429
 
468
430
 
431
+ def setup_metrics(config):
432
+ settings = config.get_settings()
433
+
434
+ # Register a no-op metrics service by default.
435
+ config.registry.registerUtility(metrics.NoOpMetricsService(), metrics.IMetricsService)
436
+
437
+ # This does not fully respect the Pyramid/ZCA patterns, but the rest of Kinto uses
438
+ # `registry.storage`, `registry.cache`, etc. Consistency seems more important.
439
+ config.registry.__class__.metrics = property(
440
+ lambda reg: reg.queryUtility(metrics.IMetricsService)
441
+ )
442
+
443
+ def deprecated_registry(self):
444
+ warnings.warn(
445
+ "``config.registry.statsd`` is now deprecated. Use ``config.registry.metrics`` instead.",
446
+ DeprecationWarning,
447
+ )
448
+ return self.metrics
449
+
450
+ config.registry.__class__.statsd = property(deprecated_registry)
451
+
452
+ def on_app_created(event):
453
+ config = event.app
454
+ metrics_service = config.registry.metrics
455
+
456
+ metrics.watch_execution_time(metrics_service, config.registry.cache, prefix="backend")
457
+ metrics.watch_execution_time(metrics_service, config.registry.storage, prefix="backend")
458
+ metrics.watch_execution_time(metrics_service, config.registry.permission, prefix="backend")
459
+
460
+ policy = config.registry.queryUtility(IAuthenticationPolicy)
461
+ if isinstance(policy, MultiAuthenticationPolicy):
462
+ for name, subpolicy in policy.get_policies():
463
+ metrics.watch_execution_time(
464
+ metrics_service, subpolicy, prefix="authentication", classname=name
465
+ )
466
+ else:
467
+ metrics.watch_execution_time(metrics_service, policy, prefix="authentication")
468
+
469
+ config.add_subscriber(on_app_created, ApplicationCreated)
470
+
471
+ def on_new_response(event):
472
+ request = event.request
473
+ metrics_service = config.registry.metrics
474
+
475
+ try:
476
+ endpoint = utils.strip_uri_prefix(request.path)
477
+ except UnicodeDecodeError as e:
478
+ # This `on_new_response` callback is also called when a HTTP 400
479
+ # is returned because of an invalid UTF-8 path. We still want metrics.
480
+ endpoint = str(e)
481
+
482
+ # Count unique users.
483
+ user_id = request.prefixed_userid
484
+ if user_id:
485
+ # Get rid of colons in metric packet (see #1282).
486
+ auth, user_id = user_id.split(":")
487
+ metrics_service.count("users", unique=[("auth", auth), ("userid", user_id)])
488
+
489
+ # Add extra labels to metrics, based on fields extracted from the request matchdict.
490
+ metrics_matchdict_fields = aslist(settings["metrics_matchdict_fields"])
491
+ # Turn the `id` field of object endpoints into `{resource}_id` (eg. `mushroom_id`, `bucket_id`)
492
+ enhanced_matchdict = dict(request.matchdict or {})
493
+ try:
494
+ enhanced_matchdict[request.current_resource_name + "_id"] = enhanced_matchdict.get(
495
+ "id", ""
496
+ )
497
+ except AttributeError:
498
+ # Not on a resource.
499
+ pass
500
+ metrics_matchdict_labels = [
501
+ (field, enhanced_matchdict.get(field, "")) for field in metrics_matchdict_fields
502
+ ]
503
+
504
+ # Count served requests.
505
+ metrics_service.count(
506
+ "request_summary",
507
+ unique=[
508
+ ("method", request.method.lower()),
509
+ ("endpoint", endpoint),
510
+ ("status", str(request.response.status_code)),
511
+ ]
512
+ + metrics_matchdict_labels,
513
+ )
514
+
515
+ try:
516
+ current = utils.msec_time()
517
+ duration = current - request._received_at
518
+ metrics_service.observe(
519
+ "request_duration",
520
+ duration,
521
+ labels=[("endpoint", endpoint)] + metrics_matchdict_labels,
522
+ )
523
+ except AttributeError: # pragma: no cover
524
+ # Logging was not setup in this Kinto app (unlikely but possible)
525
+ pass
526
+
527
+ # Observe response size.
528
+ metrics_service.observe(
529
+ "request_size",
530
+ len(request.response.body or b""),
531
+ labels=[("endpoint", endpoint)] + metrics_matchdict_labels,
532
+ )
533
+
534
+ # Count authentication verifications.
535
+ if hasattr(request, "authn_type"):
536
+ metrics_service.count(f"authn_type.{request.authn_type}")
537
+
538
+ # Count view calls.
539
+ service = request.current_service
540
+ if service:
541
+ metrics_service.count(f"view.{service.name}.{request.method}")
542
+
543
+ config.add_subscriber(on_new_response, NewResponse)
544
+
545
+ # While statsd is deprecated, we include its plugin by default for retro-compability.
546
+ if settings["statsd_url"]:
547
+ config.include("kinto.plugins.statsd")
548
+
549
+
469
550
  class EventActionFilter:
470
551
  def __init__(self, actions, config):
471
552
  actions = ACTIONS.from_string_list(actions)
@@ -518,11 +599,9 @@ def setup_listeners(config):
518
599
  listener_mod = config.maybe_dotted(module_value)
519
600
  listener = listener_mod.load_from_config(config, prefix)
520
601
 
521
- # If StatsD is enabled, monitor execution time of listeners.
522
- if getattr(config.registry, "statsd", None):
523
- statsd_client = config.registry.statsd
524
- key = f"listeners.{name}"
525
- listener = statsd_client.timer(key)(listener.__call__)
602
+ wrapped_listener = metrics.listener_with_timer(
603
+ config, f"listeners.{name}", listener.__call__
604
+ )
526
605
 
527
606
  # Optional filter by event action.
528
607
  actions_setting = prefix + "actions"
@@ -548,11 +627,11 @@ def setup_listeners(config):
548
627
  options = dict(for_actions=actions, for_resources=resource_names)
549
628
 
550
629
  if ACTIONS.READ in actions:
551
- config.add_subscriber(listener, ResourceRead, **options)
630
+ config.add_subscriber(wrapped_listener, ResourceRead, **options)
552
631
  actions = [a for a in actions if a != ACTIONS.READ]
553
632
 
554
633
  if len(actions) > 0:
555
- config.add_subscriber(listener, ResourceChanged, **options)
634
+ config.add_subscriber(wrapped_listener, ResourceChanged, **options)
556
635
 
557
636
 
558
637
  def load_default_settings(config, default_settings):
kinto/core/metrics.py ADDED
@@ -0,0 +1,93 @@
1
+ import types
2
+
3
+ from zope.interface import Interface, implementer
4
+
5
+ from kinto.core import utils
6
+
7
+
8
+ class IMetricsService(Interface):
9
+ """
10
+ An interface that defines the metrics service contract.
11
+ Any class implementing this must provide all its methods.
12
+ """
13
+
14
+ def timer(key):
15
+ """
16
+ Watch execution time.
17
+ """
18
+
19
+ def observe(self, key, value, labels=[]):
20
+ """
21
+ Observe a give `value` for the specified `key`.
22
+ """
23
+
24
+ def count(key, count=1, unique=None):
25
+ """
26
+ Count occurrences. If `unique` is set, overwrites the counter value
27
+ on each call.
28
+
29
+ `unique` should be of type ``list[tuple[str,str]]``.
30
+ """
31
+
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
+
60
+ def watch_execution_time(metrics_service, obj, prefix="", classname=None):
61
+ """
62
+ Decorate all methods of an object in order to watch their execution time.
63
+ Metrics will be named `{prefix}.{classname}.{method}`.
64
+ """
65
+ classname = classname or utils.classname(obj)
66
+ members = dir(obj)
67
+ for name in members:
68
+ value = getattr(obj, name)
69
+ is_method = isinstance(value, types.MethodType)
70
+ if not name.startswith("_") and is_method:
71
+ statsd_key = f"{prefix}.{classname}.{name}"
72
+ decorated_method = metrics_service.timer(statsd_key)(value)
73
+ setattr(obj, name, decorated_method)
74
+
75
+
76
+ def listener_with_timer(config, key, func):
77
+ """
78
+ Add a timer with the specified `key` on the specified `func`.
79
+ This is used to avoid evaluating `config.registry.metrics` during setup time
80
+ to avoid having to deal with initialization order and configuration committing.
81
+ """
82
+
83
+ def wrapped(*args, **kwargs):
84
+ metrics_service = config.registry.metrics
85
+ if not metrics_service:
86
+ # This only happens if `kinto.core.initialization.setup_metrics` is
87
+ # not listed in the `initialization_sequence` setting.
88
+ return func(*args, **kwargs)
89
+ # If metrics are enabled, monitor execution time of listeners.
90
+ with metrics_service.timer(key):
91
+ return func(*args, **kwargs)
92
+
93
+ return wrapped
kinto/core/statsd.py CHANGED
@@ -1,63 +1 @@
1
- import types
2
- from urllib.parse import urlparse
3
-
4
- from pyramid.exceptions import ConfigurationError
5
-
6
- from kinto.core import utils
7
-
8
-
9
- try:
10
- import statsd as statsd_module
11
- except ImportError: # pragma: no cover
12
- statsd_module = None
13
-
14
-
15
- class Client:
16
- def __init__(self, host, port, prefix):
17
- self._client = statsd_module.StatsClient(host, port, prefix=prefix)
18
-
19
- def watch_execution_time(self, obj, prefix="", classname=None):
20
- classname = classname or utils.classname(obj)
21
- members = dir(obj)
22
- for name in members:
23
- value = getattr(obj, name)
24
- is_method = isinstance(value, types.MethodType)
25
- if not name.startswith("_") and is_method:
26
- statsd_key = f"{prefix}.{classname}.{name}"
27
- decorated_method = self.timer(statsd_key)(value)
28
- setattr(obj, name, decorated_method)
29
-
30
- def timer(self, key):
31
- return self._client.timer(key)
32
-
33
- def count(self, key, count=1, unique=None):
34
- if unique is None:
35
- return self._client.incr(key, count=count)
36
- else:
37
- return self._client.set(key, unique)
38
-
39
-
40
- def statsd_count(request, count_key):
41
- statsd = request.registry.statsd
42
- if statsd:
43
- statsd.count(count_key)
44
-
45
-
46
- def load_from_config(config):
47
- # If this is called, it means that a ``statsd_url`` was specified in settings.
48
- # (see ``kinto.core.initialization``)
49
- # Raise a proper error if the ``statsd`` module is not installed.
50
- if statsd_module is None:
51
- error_msg = "Please install Kinto with monitoring dependencies (e.g. statsd package)"
52
- raise ConfigurationError(error_msg)
53
-
54
- settings = config.get_settings()
55
- uri = settings["statsd_url"]
56
- uri = urlparse(uri)
57
-
58
- if settings["project_name"] != "":
59
- prefix = settings["project_name"]
60
- else:
61
- prefix = settings["statsd_prefix"]
62
-
63
- return Client(uri.hostname, uri.port, prefix)
1
+ from kinto.plugins.statsd import load_from_config # noqa: F401
kinto/core/testing.py CHANGED
@@ -8,15 +8,19 @@ import webtest
8
8
  from cornice import errors as cornice_errors
9
9
  from pyramid.url import parse_url_overrides
10
10
 
11
- from kinto.core import DEFAULT_SETTINGS, statsd
11
+ from kinto.core import DEFAULT_SETTINGS
12
12
  from kinto.core.storage import generators
13
13
  from kinto.core.utils import encode64, follow_subrequest, memcache, sqlalchemy
14
+ from kinto.plugins import prometheus, statsd
14
15
 
15
16
 
16
17
  skip_if_ci = unittest.skipIf("CI" in os.environ, "ci")
17
18
  skip_if_no_postgresql = unittest.skipIf(sqlalchemy is None, "postgresql is not installed.")
18
19
  skip_if_no_memcached = unittest.skipIf(memcache is None, "memcached is not installed.")
19
20
  skip_if_no_statsd = unittest.skipIf(not statsd.statsd_module, "statsd is not installed.")
21
+ skip_if_no_prometheus = unittest.skipIf(
22
+ not prometheus.prometheus_module, "prometheus is not installed."
23
+ )
20
24
 
21
25
 
22
26
  class DummyRequest(mock.MagicMock):
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