Browse Source

HUE-8744 [metrics] Add django-prometheus and prometheus_client to ext-py

Romain 6 years ago
parent
commit
04ed8ec022
63 changed files with 5678 additions and 0 deletions
  1. 31 0
      desktop/core/ext-py/django-prometheus-1.0.15/PKG-INFO
  2. 146 0
      desktop/core/ext-py/django-prometheus-1.0.15/README.md
  3. 19 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/__init__.py
  4. 24 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/apps.py
  5. 0 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/cache/__init__.py
  6. 0 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/cache/backends/__init__.py
  7. 20 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/cache/backends/django_memcached_consul.py
  8. 17 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/cache/backends/filebased.py
  9. 17 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/cache/backends/locmem.py
  10. 19 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/cache/backends/memcached.py
  11. 36 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/cache/backends/redis.py
  12. 15 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/cache/metrics.py
  13. 2 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/db/__init__.py
  14. 0 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/db/backends/__init__.py
  15. 0 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/db/backends/mysql/__init__.py
  16. 18 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/db/backends/mysql/base.py
  17. 0 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/db/backends/postgresql/__init__.py
  18. 34 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/db/backends/postgresql/base.py
  19. 0 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/db/backends/sqlite3/__init__.py
  20. 11 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/db/backends/sqlite3/base.py
  21. 72 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/db/common.py
  22. 31 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/db/metrics.py
  23. 118 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/exports.py
  24. 183 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/middleware.py
  25. 47 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/migrations.py
  26. 44 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/models.py
  27. 0 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/tests/__init__.py
  28. 24 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/tests/test_django_prometheus.py
  29. 25 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/tests/test_exports.py
  30. 150 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/tests/test_testutils.py
  31. 156 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/testutils.py
  32. 7 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/urls.py
  33. 29 0
      desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/utils.py
  34. 4 0
      desktop/core/ext-py/django-prometheus-1.0.15/setup.cfg
  35. 44 0
      desktop/core/ext-py/django-prometheus-1.0.15/setup.py
  36. 29 0
      desktop/core/ext-py/prometheus_client-0.7.1/PKG-INFO
  37. 542 0
      desktop/core/ext-py/prometheus_client-0.7.1/README.md
  38. 61 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/__init__.py
  39. 0 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/bridge/__init__.py
  40. 82 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/bridge/graphite.py
  41. 68 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/context_managers.py
  42. 32 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/core.py
  43. 427 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/decorator.py
  44. 368 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/exposition.py
  45. 48 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/gc_collector.py
  46. 649 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/metrics.py
  47. 325 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/metrics_core.py
  48. 144 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/mmap_dict.py
  49. 161 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/multiprocess.py
  50. 0 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/openmetrics/__init__.py
  51. 65 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/openmetrics/exposition.py
  52. 575 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/openmetrics/parser.py
  53. 232 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/parser.py
  54. 59 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/platform_collector.py
  55. 98 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/process_collector.py
  56. 123 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/registry.py
  57. 43 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/samples.py
  58. 3 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/twisted/__init__.py
  59. 20 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/twisted/_exposition.py
  60. 23 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/utils.py
  61. 110 0
      desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/values.py
  62. 4 0
      desktop/core/ext-py/prometheus_client-0.7.1/setup.cfg
  63. 44 0
      desktop/core/ext-py/prometheus_client-0.7.1/setup.py

+ 31 - 0
desktop/core/ext-py/django-prometheus-1.0.15/PKG-INFO

@@ -0,0 +1,31 @@
+Metadata-Version: 1.1
+Name: django-prometheus
+Version: 1.0.15
+Summary: Django middlewares to monitor your application with Prometheus.io.
+Home-page: http://github.com/korfuri/django-prometheus
+Author: Uriel Corfa
+Author-email: uriel@corfa.fr
+License: Apache
+Description: Django-Prometheus
+        
+        This library contains code to expose some monitoring metrics relevant
+        to Django internals so they can be monitored by Prometheus.io.
+        
+        See https://github.com/korfuri/django-prometheus for usage
+        instructions.
+        
+Keywords: django monitoring prometheus
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: Information Technology
+Classifier: Intended Audience :: System Administrators
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Framework :: Django
+Classifier: Topic :: System :: Monitoring
+Classifier: License :: OSI Approved :: Apache Software License

+ 146 - 0
desktop/core/ext-py/django-prometheus-1.0.15/README.md

@@ -0,0 +1,146 @@
+# django-prometheus
+Export Django monitoring metrics for Prometheus.io
+
+[![PyPI version](https://badge.fury.io/py/django-prometheus.svg)](http://badge.fury.io/py/django-prometheus)
+[![Build Status](https://travis-ci.org/korfuri/django-prometheus.svg?branch=master)](https://travis-ci.org/korfuri/django-prometheus)
+
+## Usage
+
+### Requirements
+
+* Django >= 1.8
+
+### Installation
+
+Install with:
+```shell
+pip install django-prometheus
+```
+
+Or, if you're using a development version cloned from this repository:
+```shell
+python path-to-where-you-cloned-django-prometheus/setup.py install
+```
+
+This will install [prometheus_client](https://github.com/prometheus/client_python) as a dependency.
+
+### Quickstart
+
+In your settings.py:
+
+```python
+INSTALLED_APPS = (
+   ...
+   'django_prometheus',
+   ...
+)
+
+MIDDLEWARE_CLASSES = (
+    'django_prometheus.middleware.PrometheusBeforeMiddleware',
+    # All your other middlewares go here, including the default
+    # middlewares like SessionMiddleware, CommonMiddleware,
+    # CsrfViewmiddleware, SecurityMiddleware, etc.
+    'django_prometheus.middleware.PrometheusAfterMiddleware',
+)
+```
+
+In your urls.py:
+
+```python
+urlpatterns = [
+    ...
+    url('', include('django_prometheus.urls')),
+]
+```
+
+### Monitoring your databases
+
+SQLite, MySQL, and PostgreSQL databases can be monitored. Just
+replace the `ENGINE` property of your database, replacing
+`django.db.backends` with `django_prometheus.db.backends`.
+
+```python
+DATABASES = {
+    'default': {
+        'ENGINE': 'django_prometheus.db.backends.sqlite3',
+        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+    },
+}
+```
+
+### Monitoring your caches
+
+Filebased, memcached, redis caches can be monitored. Just replace
+the cache backend to use the one provided by django_prometheus
+`django.core.cache.backends` with `django_prometheus.cache.backends`.
+
+```python
+CACHES = {
+    'default': {
+        'BACKEND': 'django_prometheus.cache.backends.filebased.FileBasedCache',
+        'LOCATION': '/var/tmp/django_cache',
+    }
+}
+```
+
+### Monitoring your models
+
+You may want to monitor the creation/deletion/update rate for your
+model. This can be done by adding a mixin to them. This is safe to do
+on existing models (it does not require a migration).
+
+If your model is:
+
+```python
+class Dog(models.Model):
+    name = models.CharField(max_length=100, unique=True)
+    breed = models.CharField(max_length=100, blank=True, null=True)
+    age = models.PositiveIntegerField(blank=True, null=True)
+```
+
+Just add the `ExportModelOperationsMixin` as such:
+
+```python
+from django_prometheus.models import ExportModelOperationsMixin
+
+class Dog(ExportModelOperationsMixin('dog'), models.Model):
+    name = models.CharField(max_length=100, unique=True)
+    breed = models.CharField(max_length=100, blank=True, null=True)
+    age = models.PositiveIntegerField(blank=True, null=True)
+```
+
+This will export 3 metrics, `django_model_inserts_total{model="dog"}`,
+`django_model_updates_total{model="dog"}` and
+`django_model_deletes_total{model="dog"}`.
+
+Note that the exported metrics are counters of creations,
+modifications and deletions done in the current process. They are not
+gauges of the number of objects in the model.
+
+Starting with Django 1.7, migrations are also monitored. Two gauges
+are exported, `django_migrations_applied_by_connection` and
+`django_migrations_unapplied_by_connection`. You may want to alert if
+there are unapplied migrations.
+
+### Monitoring and aggregating the metrics
+
+Prometheus is quite easy to set up. An example prometheus.conf to
+scrape `127.0.0.1:8001` can be found in `examples/prometheus`.
+
+Here's an example of a PromDash displaying some of the metrics
+collected by django-prometheus:
+
+![Example dashboard](https://raw.githubusercontent.com/korfuri/django-prometheus/master/examples/django-promdash.png)
+
+## Adding your own metrics
+
+You can add application-level metrics in your code by using
+[prometheus_client](https://github.com/prometheus/client_python)
+directly. The exporter is global and will pick up your metrics.
+
+To add metrics to the Django internals, the easiest way is to extend
+django-prometheus' classes. Please consider contributing your metrics,
+pull requests are welcome. Make sure to read the Prometheus best
+practices on
+[instrumentation](http://prometheus.io/docs/practices/instrumentation/)
+and [naming](http://prometheus.io/docs/practices/naming/).

+ 19 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/__init__.py

@@ -0,0 +1,19 @@
+"""Django-Prometheus
+
+https://github.com/korfuri/django-prometheus
+"""
+
+# Import all files that define metrics. This has the effect that
+# `import django_prometheus` will always instanciate all metric
+# objects right away.
+import django_prometheus.middleware
+import django_prometheus.models
+
+# Import pip_prometheus to export the pip metrics automatically.
+try:
+    import pip_prometheus
+except ImportError:
+    # If people don't have pip, don't export anything.
+    pass
+
+default_app_config = 'django_prometheus.apps.DjangoPrometheusConfig'

+ 24 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/apps.py

@@ -0,0 +1,24 @@
+from django.apps import AppConfig
+from django.conf import settings
+from django_prometheus.exports import SetupPrometheusExportsFromConfig
+from django_prometheus.migrations import ExportMigrations
+# unused import to force instantiating the metric objects at startup.
+import django_prometheus
+
+
+class DjangoPrometheusConfig(AppConfig):
+    name = 'django_prometheus'
+    verbose_name = 'Django-Prometheus'
+
+    def ready(self):
+        """Initializes the Prometheus exports if they are enabled in the config.
+
+        Note that this is called even for other management commands
+        than `runserver`. As such, it is possible to scrape the
+        metrics of a running `manage.py test` or of another command,
+        which shouldn't be done for real monitoring (since these jobs
+        are usually short-lived), but can be useful for debugging.
+        """
+        SetupPrometheusExportsFromConfig()
+        if getattr(settings, 'PROMETHEUS_EXPORT_MIGRATIONS', True):
+            ExportMigrations()

+ 0 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/cache/__init__.py


+ 0 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/cache/backends/__init__.py


+ 20 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/cache/backends/django_memcached_consul.py

@@ -0,0 +1,20 @@
+from __future__ import absolute_import
+from django_memcached_consul import memcached
+from django_prometheus.cache.metrics import (
+    django_cache_get_total, django_cache_hits_total, django_cache_misses_total)
+
+
+class MemcachedCache(memcached.MemcachedCache):
+    """Inherit django_memcached_consul to add metrics about hit/miss ratio"""
+
+    def get(self, key, default=None, version=None):
+        django_cache_get_total.labels(backend='django_memcached_consul').inc()
+        cached = super(MemcachedCache, self).get(
+            key, default=None, version=version)
+        if cached is not None:
+            django_cache_hits_total.labels(
+                backend='django_memcached_consul').inc()
+        else:
+            django_cache_misses_total.labels(
+                backend='django_memcached_consul').inc()
+        return cached or default

+ 17 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/cache/backends/filebased.py

@@ -0,0 +1,17 @@
+from django.core.cache.backends import filebased
+from django_prometheus.cache.metrics import (
+    django_cache_get_total, django_cache_hits_total, django_cache_misses_total)
+
+
+class FileBasedCache(filebased.FileBasedCache):
+    """Inherit filebased cache to add metrics about hit/miss ratio"""
+
+    def get(self, key, default=None, version=None):
+        django_cache_get_total.labels(backend='filebased').inc()
+        cached = super(FileBasedCache, self).get(
+            key, default=None, version=version)
+        if cached is not None:
+            django_cache_hits_total.labels(backend='filebased').inc()
+        else:
+            django_cache_misses_total.labels(backend='filebased').inc()
+        return cached or default

+ 17 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/cache/backends/locmem.py

@@ -0,0 +1,17 @@
+from django.core.cache.backends import locmem
+from django_prometheus.cache.metrics import (
+    django_cache_get_total, django_cache_hits_total, django_cache_misses_total)
+
+
+class LocMemCache(locmem.LocMemCache):
+    """Inherit filebased cache to add metrics about hit/miss ratio"""
+
+    def get(self, key, default=None, version=None):
+        django_cache_get_total.labels(backend='locmem').inc()
+        cached = super(LocMemCache, self).get(
+            key, default=None, version=version)
+        if cached is not None:
+            django_cache_hits_total.labels(backend='locmem').inc()
+        else:
+            django_cache_misses_total.labels(backend='locmem').inc()
+        return cached or default

+ 19 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/cache/backends/memcached.py

@@ -0,0 +1,19 @@
+from django.core.cache.backends import memcached
+from django_prometheus.cache.metrics import (
+    django_cache_get_total, django_cache_hits_total, django_cache_misses_total)
+
+
+class MemcachedCache(memcached.MemcachedCache):
+    """Inherit memcached to add metrics about hit/miss ratio"""
+
+    def get(self, key, default=None, version=None):
+        django_cache_get_total.labels(backend='memcached').inc()
+        cached = super(MemcachedCache, self).get(
+            key, default=None, version=version)
+        if cached is not None:
+            django_cache_hits_total.labels(
+                backend='memcached').inc()
+        else:
+            django_cache_misses_total.labels(
+                backend='memcached').inc()
+        return cached or default

+ 36 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/cache/backends/redis.py

@@ -0,0 +1,36 @@
+from django_redis import cache, exceptions
+
+from django_prometheus.cache.metrics import (
+    django_cache_get_fail_total,
+    django_cache_get_total,
+    django_cache_hits_total,
+    django_cache_misses_total,
+)
+
+
+class RedisCache(cache.RedisCache):
+    """Inherit redis to add metrics about hit/miss/interruption ratio"""
+
+    @cache.omit_exception
+    def get(self, key, default=None, version=None, client=None):
+        try:
+            django_cache_get_total.labels(backend='redis').inc()
+            cached = self.client.get(
+                key,
+                default=None,
+                version=version,
+                client=client
+            )
+        except exceptions.ConnectionInterrupted as e:
+            django_cache_get_fail_total.labels(backend='redis').inc()
+            if cache.DJANGO_REDIS_IGNORE_EXCEPTIONS or self._ignore_exceptions:
+                if cache.DJANGO_REDIS_LOG_IGNORED_EXCEPTIONS:
+                    cache.logger.error(str(e))
+                return default
+            raise
+        else:
+            if cached is not None:
+                django_cache_hits_total.labels(backend='redis').inc()
+            else:
+                django_cache_misses_total.labels(backend='redis').inc()
+            return cached or default

+ 15 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/cache/metrics.py

@@ -0,0 +1,15 @@
+from prometheus_client import Counter
+
+django_cache_get_total = Counter(
+    'django_cache_get_total', 'Total get requests on cache', ['backend']
+)
+django_cache_hits_total = Counter(
+    'django_cache_get_hits_total', 'Total hits on cache', ['backend']
+)
+django_cache_misses_total = Counter(
+    'django_cache_get_misses_total', 'Total misses on cache', ['backend']
+)
+django_cache_get_fail_total = Counter(
+    'django_cache_get_fail_total',
+    'Total get request failures by cache', ['backend']
+)

+ 2 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/db/__init__.py

@@ -0,0 +1,2 @@
+# Import all metrics
+from django_prometheus.db.metrics import *

+ 0 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/db/backends/__init__.py


+ 0 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/db/backends/mysql/__init__.py


+ 18 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/db/backends/mysql/base.py

@@ -0,0 +1,18 @@
+from django_prometheus.db.common import (
+    DatabaseWrapperMixin, ExportingCursorWrapper)
+from django.db.backends.mysql import base
+
+
+class DatabaseFeatures(base.DatabaseFeatures):
+    """Our database has the exact same features as the base one."""
+    pass
+
+
+class DatabaseWrapper(DatabaseWrapperMixin, base.DatabaseWrapper):
+    CURSOR_CLASS = base.CursorWrapper
+
+    def create_cursor(self, name=None):
+        cursor = self.connection.cursor()
+        CursorWrapper = ExportingCursorWrapper(
+            self.CURSOR_CLASS, self.alias, self.vendor)
+        return CursorWrapper(cursor)

+ 0 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/db/backends/postgresql/__init__.py


+ 34 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/db/backends/postgresql/base.py

@@ -0,0 +1,34 @@
+import django
+import psycopg2.extensions
+
+from django_prometheus.db.common import DatabaseWrapperMixin, \
+    ExportingCursorWrapper
+
+if django.VERSION >= (1, 9):
+    from django.db.backends.postgresql import base
+else:
+    from django.db.backends.postgresql_psycopg2 import base
+
+
+class DatabaseFeatures(base.DatabaseFeatures):
+    """Our database has the exact same features as the base one."""
+    pass
+
+
+class DatabaseWrapper(DatabaseWrapperMixin, base.DatabaseWrapper):
+    def get_connection_params(self):
+        conn_params = super(DatabaseWrapper, self).get_connection_params()
+        conn_params['cursor_factory'] = ExportingCursorWrapper(
+            psycopg2.extensions.cursor,
+            self.alias,
+            self.vendor,
+        )
+        return conn_params
+
+    def create_cursor(self, name=None):
+        # cursor_factory is a kwarg to connect() so restore create_cursor()'s
+        # default behavior
+        if django.VERSION >= (1, 11, 0):
+            return base.DatabaseWrapper.create_cursor(self, name=name)
+        else:
+            return base.DatabaseWrapper.create_cursor(self)

+ 0 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/db/backends/sqlite3/__init__.py


+ 11 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/db/backends/sqlite3/base.py

@@ -0,0 +1,11 @@
+from django_prometheus.db.common import DatabaseWrapperMixin
+from django.db.backends.sqlite3 import base
+
+
+class DatabaseFeatures(base.DatabaseFeatures):
+    """Our database has the exact same features as the base one."""
+    pass
+
+
+class DatabaseWrapper(DatabaseWrapperMixin, base.DatabaseWrapper):
+    CURSOR_CLASS = base.SQLiteCursorWrapper

+ 72 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/db/common.py

@@ -0,0 +1,72 @@
+from django_prometheus.db import (
+    connections_total, execute_total, execute_many_total, errors_total,
+    connection_errors_total)
+
+
+class ExceptionCounterByType(object):
+    """A context manager that counts exceptions by type.
+
+    Exceptions increment the provided counter, whose last label's name
+    must match the `type_label` argument.
+
+    In other words:
+
+    c = Counter('http_request_exceptions_total', 'Counter of exceptions',
+                ['method', 'type'])
+    with ExceptionCounterByType(c, extra_labels={'method': 'GET'}):
+        handle_get_request()
+    """
+
+    def __init__(self, counter, type_label='type', extra_labels=None):
+        self._counter = counter
+        self._type_label = type_label
+        self._labels = extra_labels
+
+    def __enter__(self):
+        pass
+
+    def __exit__(self, typ, value, traceback):
+        if typ is not None:
+            self._labels.update({self._type_label: typ.__name__})
+            self._counter.labels(**self._labels).inc()
+
+
+class DatabaseWrapperMixin(object):
+    """Extends the DatabaseWrapper to count connections and cursors."""
+
+    def get_new_connection(self, *args, **kwargs):
+        connections_total.labels(self.alias, self.vendor).inc()
+        try:
+            return super(DatabaseWrapperMixin, self).get_new_connection(
+                *args, **kwargs)
+        except:
+            connection_errors_total.labels(self.alias, self.vendor).inc()
+            raise
+
+    def create_cursor(self, name=None):
+        return self.connection.cursor(factory=ExportingCursorWrapper(
+            self.CURSOR_CLASS, self.alias, self.vendor))
+
+
+def ExportingCursorWrapper(cursor_class, alias, vendor):
+    """Returns a CursorWrapper class that knows its database's alias and
+    vendor name.
+    """
+
+    class CursorWrapper(cursor_class):
+        """Extends the base CursorWrapper to count events."""
+
+        def execute(self, *args, **kwargs):
+            execute_total.labels(alias, vendor).inc()
+            with ExceptionCounterByType(errors_total, extra_labels={
+                    'alias': alias, 'vendor': vendor}):
+                return super(CursorWrapper, self).execute(*args, **kwargs)
+
+        def executemany(self, query, param_list, *args, **kwargs):
+            execute_total.labels(alias, vendor).inc(len(param_list))
+            execute_many_total.labels(alias, vendor).inc(len(param_list))
+            with ExceptionCounterByType(errors_total, extra_labels={
+                    'alias': alias, 'vendor': vendor}):
+                return super(CursorWrapper, self).executemany(
+                    query, param_list, *args, **kwargs)
+    return CursorWrapper

+ 31 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/db/metrics.py

@@ -0,0 +1,31 @@
+from prometheus_client import Counter
+
+
+connections_total = Counter(
+    'django_db_new_connections_total',
+    'Counter of created connections by database and by vendor.',
+    ['alias', 'vendor'])
+
+connection_errors_total = Counter(
+    'django_db_new_connection_errors_total',
+    'Counter of connection failures by database and by vendor.',
+    ['alias', 'vendor'])
+
+execute_total = Counter(
+    'django_db_execute_total',
+    ('Counter of executed statements by database and by vendor, including'
+     ' bulk executions.'),
+    ['alias', 'vendor'])
+
+
+execute_many_total = Counter(
+    'django_db_execute_many_total',
+    ('Counter of executed statements in bulk operations by database and'
+     ' by vendor.'),
+    ['alias', 'vendor'])
+
+
+errors_total = Counter(
+    'django_db_errors_total',
+    ('Counter of execution errors by database, vendor and exception type.'),
+    ['alias', 'vendor', 'type'])

+ 118 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/exports.py

@@ -0,0 +1,118 @@
+from django.http import HttpResponse
+from django.conf import settings
+from prometheus_client import multiprocess
+
+try:
+    # Python 2
+    from BaseHTTPServer import HTTPServer
+except ImportError:
+    # Python 3
+    from http.server import HTTPServer
+import socket
+import logging
+import os
+import prometheus_client
+import threading
+
+
+logger = logging.getLogger(__name__)
+
+
+def SetupPrometheusEndpointOnPort(port, addr=''):
+    """Exports Prometheus metrics on an HTTPServer running in its own thread.
+
+    The server runs on the given port and is by default listenning on
+    all interfaces. This HTTPServer is fully independent of Django and
+    its stack. This offers the advantage that even if Django becomes
+    unable to respond, the HTTPServer will continue to function and
+    export metrics. However, this also means that the features
+    offered by Django (like middlewares or WSGI) can't be used.
+
+    Now here's the really weird part. When Django runs with the
+    auto-reloader enabled (which is the default, you can disable it
+    with `manage.py runserver --noreload`), it forks and executes
+    manage.py twice. That's wasteful but usually OK. It starts being a
+    problem when you try to open a port, like we do. We can detect
+    that we're running under an autoreloader through the presence of
+    the RUN_MAIN environment variable, so we abort if we're trying to
+    export under an autoreloader and trying to open a port.
+    """
+    assert os.environ.get('RUN_MAIN') != 'true', (
+        'The thread-based exporter can\'t be safely used when django\'s '
+        'autoreloader is active. Use the URL exporter, or start django '
+        'with --noreload. See documentation/exports.md.')
+    prometheus_client.start_http_server(port, addr=addr)
+
+
+class PrometheusEndpointServer(threading.Thread):
+    """A thread class that holds an http and makes it serve_forever()."""
+    def __init__(self, httpd, *args, **kwargs):
+        self.httpd = httpd
+        super(PrometheusEndpointServer, self).__init__(*args, **kwargs)
+
+    def run(self):
+        self.httpd.serve_forever()
+
+
+def SetupPrometheusEndpointOnPortRange(port_range, addr=''):
+    """Like SetupPrometheusEndpointOnPort, but tries several ports.
+
+    This is useful when you're running Django as a WSGI application
+    with multiple processes and you want Prometheus to discover all
+    workers. Each worker will grab a port and you can use Prometheus
+    to aggregate across workers.
+
+    port_range may be any iterable object that contains a list of
+    ports. Typically this would be an xrange of contiguous ports.
+
+    As soon as one port is found that can serve, use this one and stop
+    trying.
+
+    The same caveats regarding autoreload apply. Do not use this when
+    Django's autoreloader is active.
+
+    """
+    assert os.environ.get('RUN_MAIN') != 'true', (
+        'The thread-based exporter can\'t be safely used when django\'s '
+        'autoreloader is active. Use the URL exporter, or start django '
+        'with --noreload. See documentation/exports.md.')
+    for port in port_range:
+        try:
+            httpd = HTTPServer((addr, port), prometheus_client.MetricsHandler)
+        except (OSError, socket.error):
+            # Python 2 raises socket.error, in Python 3 socket.error is an
+            # alias for OSError
+            continue  # Try next port
+        thread = PrometheusEndpointServer(httpd)
+        thread.daemon = True
+        thread.start()
+        logger.info('Exporting Prometheus /metrics/ on port %s' % port)
+        return  # Stop trying ports at this point
+
+
+def SetupPrometheusExportsFromConfig():
+    """Exports metrics so Prometheus can collect them."""
+    port = getattr(settings, 'PROMETHEUS_METRICS_EXPORT_PORT', None)
+    port_range = getattr(
+        settings, 'PROMETHEUS_METRICS_EXPORT_PORT_RANGE', None)
+    addr = getattr(settings, 'PROMETHEUS_METRICS_EXPORT_ADDRESS', '')
+    if port_range:
+        SetupPrometheusEndpointOnPortRange(port_range, addr)
+    elif port:
+        SetupPrometheusEndpointOnPort(port, addr)
+
+
+def ExportToDjangoView(request):
+    """Exports /metrics as a Django view.
+
+    You can use django_prometheus.urls to map /metrics to this view.
+    """
+    if 'prometheus_multiproc_dir' in os.environ:
+        registry = prometheus_client.CollectorRegistry()
+        multiprocess.MultiProcessCollector(registry)
+    else:
+        registry = prometheus_client.REGISTRY
+    metrics_page = prometheus_client.generate_latest(registry)
+    return HttpResponse(
+        metrics_page,
+        content_type=prometheus_client.CONTENT_TYPE_LATEST)

+ 183 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/middleware.py

@@ -0,0 +1,183 @@
+from prometheus_client import Counter, Histogram
+from django_prometheus.utils import Time, TimeSince, PowersOf
+import django
+
+if django.VERSION >= (1, 10, 0):
+    from django.utils.deprecation import MiddlewareMixin
+else:
+    MiddlewareMixin = object
+
+requests_total = Counter(
+    'django_http_requests_before_middlewares_total',
+    'Total count of requests before middlewares run.')
+responses_total = Counter(
+    'django_http_responses_before_middlewares_total',
+    'Total count of responses before middlewares run.')
+requests_latency_before = Histogram(
+    'django_http_requests_latency_including_middlewares_seconds',
+    ('Histogram of requests processing time (including middleware '
+     'processing time).'))
+requests_unknown_latency_before = Counter(
+    'django_http_requests_unknown_latency_including_middlewares_total',
+    ('Count of requests for which the latency was unknown (when computing '
+     'django_http_requests_latency_including_middlewares_seconds).'))
+
+
+class PrometheusBeforeMiddleware(MiddlewareMixin):
+
+    """Monitoring middleware that should run before other middlewares."""
+    def process_request(self, request):
+        requests_total.inc()
+        request.prometheus_before_middleware_event = Time()
+
+    def process_response(self, request, response):
+        responses_total.inc()
+        if hasattr(request, 'prometheus_before_middleware_event'):
+            requests_latency_before.observe(TimeSince(
+                request.prometheus_before_middleware_event))
+        else:
+            requests_unknown_latency_before.inc()
+        return response
+
+
+requests_latency_by_view_method = Histogram(
+    'django_http_requests_latency_seconds_by_view_method',
+    'Histogram of request processing time labelled by view.',
+    ['view', 'method'])
+requests_unknown_latency = Counter(
+    'django_http_requests_unknown_latency_total',
+    'Count of requests for which the latency was unknown.')
+# Set in process_request
+ajax_requests = Counter(
+    'django_http_ajax_requests_total',
+    'Count of AJAX requests.')
+requests_by_method = Counter(
+    'django_http_requests_total_by_method',
+    'Count of requests by method.',
+    ['method'])
+requests_by_transport = Counter(
+    'django_http_requests_total_by_transport',
+    'Count of requests by transport.',
+    ['transport'])
+# Set in process_view
+requests_by_view_transport_method = Counter(
+    'django_http_requests_total_by_view_transport_method',
+    'Count of requests by view, transport, method.',
+    ['view', 'transport', 'method'])
+requests_body_bytes = Histogram(
+    'django_http_requests_body_total_bytes',
+    'Histogram of requests by body size.',
+    buckets=PowersOf(2, 30))
+# Set in process_template_response
+responses_by_templatename = Counter(
+    'django_http_responses_total_by_templatename',
+    'Count of responses by template name.',
+    ['templatename'])
+# Set in process_response
+responses_by_status = Counter(
+    'django_http_responses_total_by_status',
+    'Count of responses by status.',
+    ['status'])
+responses_body_bytes = Histogram(
+    'django_http_responses_body_total_bytes',
+    'Histogram of responses by body size.',
+    buckets=PowersOf(2, 30))
+responses_by_charset = Counter(
+    'django_http_responses_total_by_charset',
+    'Count of responses by charset.',
+    ['charset'])
+responses_streaming = Counter(
+    'django_http_responses_streaming_total',
+    'Count of streaming responses.')
+# Set in process_exception
+exceptions_by_type = Counter(
+    'django_http_exceptions_total_by_type',
+    'Count of exceptions by object type.',
+    ['type'])
+exceptions_by_view = Counter(
+    'django_http_exceptions_total_by_view',
+    'Count of exceptions by view.',
+    ['view_name'])
+
+
+class PrometheusAfterMiddleware(MiddlewareMixin):
+
+    """Monitoring middleware that should run after other middlewares."""
+    def _transport(self, request):
+        return 'https' if request.is_secure() else 'http'
+
+    def _method(self, request):
+        m = request.method
+        if m not in ('GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'TRACE',
+                     'OPTIONS', 'CONNECT', 'PATCH'):
+            return '<invalid method>'
+        return m
+
+    def process_request(self, request):
+        transport = self._transport(request)
+        method = self._method(request)
+        requests_by_method.labels(method).inc()
+        requests_by_transport.labels(transport).inc()
+        if request.is_ajax():
+            ajax_requests.inc()
+        content_length = int(request.META.get('CONTENT_LENGTH') or 0)
+        requests_body_bytes.observe(content_length)
+        request.prometheus_after_middleware_event = Time()
+
+    def _get_view_name(self, request):
+        view_name = "<unnamed view>"
+        if hasattr(request, 'resolver_match'):
+            if request.resolver_match is not None:
+                if request.resolver_match.view_name is not None:
+                    view_name = request.resolver_match.view_name
+        return view_name
+
+    def process_view(self, request, view_func, *view_args, **view_kwargs):
+        transport = self._transport(request)
+        method = self._method(request)
+        if hasattr(request, 'resolver_match'):
+            name = request.resolver_match.view_name or '<unnamed view>'
+            requests_by_view_transport_method.labels(
+                name, transport, method).inc()
+
+    def process_template_response(self, request, response):
+        if hasattr(response, 'template_name'):
+            responses_by_templatename.labels(str(
+                response.template_name)).inc()
+        return response
+
+    def process_response(self, request, response):
+        responses_by_status.labels(str(response.status_code)).inc()
+        if hasattr(response, 'charset'):
+            responses_by_charset.labels(str(response.charset)).inc()
+        if hasattr(response, 'streaming') and response.streaming:
+            responses_streaming.inc()
+        if hasattr(response, 'content'):
+            responses_body_bytes.observe(len(response.content))
+        if hasattr(request, 'prometheus_after_middleware_event'):
+            requests_latency_by_view_method\
+                .labels(
+                    view=self._get_view_name(request),
+                    method=request.method)\
+                .observe(TimeSince(
+                    request.prometheus_after_middleware_event
+                ))
+        else:
+            requests_unknown_latency.inc()
+        return response
+
+    def process_exception(self, request, exception):
+        exceptions_by_type.labels(type(exception).__name__).inc()
+        if hasattr(request, 'resolver_match'):
+            name = request.resolver_match.view_name or '<unnamed view>'
+            exceptions_by_view.labels(name).inc()
+        if hasattr(request, 'prometheus_after_middleware_event'):
+            requests_latency_by_view_method\
+                .labels(
+                    view=self._get_view_name(request),
+                    method=request.method)\
+                .observe(TimeSince(
+                    request.prometheus_after_middleware_event
+                ))
+        else:
+            requests_unknown_latency.inc()

+ 47 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/migrations.py

@@ -0,0 +1,47 @@
+from django.db import connections
+from django.db.backends.dummy.base import DatabaseWrapper
+from prometheus_client import Gauge
+
+unapplied_migrations = Gauge(
+    'django_migrations_unapplied_total',
+    'Count of unapplied migrations by database connection',
+    ['connection'])
+
+applied_migrations = Gauge(
+    'django_migrations_applied_total',
+    'Count of applied migrations by database connection',
+    ['connection'])
+
+
+def ExportMigrationsForDatabase(alias, executor):
+    plan = executor.migration_plan(executor.loader.graph.leaf_nodes())
+    unapplied_migrations.labels(alias).set(len(plan))
+    applied_migrations.labels(alias).set(len(
+        executor.loader.applied_migrations))
+
+
+def ExportMigrations():
+    """Exports counts of unapplied migrations.
+
+    This is meant to be called during app startup, ideally by
+    django_prometheus.apps.AppConfig.
+    """
+
+    # Import MigrationExecutor lazily. MigrationExecutor checks at
+    # import time that the apps are ready, and they are not when
+    # django_prometheus is imported. ExportMigrations() should be
+    # called in AppConfig.ready(), which signals that all apps are
+    # ready.
+    from django.db.migrations.executor import MigrationExecutor
+
+    if 'default' in connections and (
+            type(connections['default']) == DatabaseWrapper):
+        # This is the case where DATABASES = {} in the configuration,
+        # i.e. the user is not using any databases. Django "helpfully"
+        # adds a dummy database and then throws when you try to
+        # actually use it. So we don't do anything, because trying to
+        # export stats would crash the app on startup.
+        return
+    for alias in connections.databases:
+        executor = MigrationExecutor(connections[alias])
+        ExportMigrationsForDatabase(alias, executor)

+ 44 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/models.py

@@ -0,0 +1,44 @@
+from prometheus_client import Counter
+
+model_inserts = Counter(
+    'django_model_inserts_total',
+    'Number of insert operations by model.',
+    ['model'])
+
+model_updates = Counter(
+    'django_model_updates_total',
+    'Number of update operations by model.',
+    ['model'])
+
+model_deletes = Counter(
+    'django_model_deletes_total',
+    'Number of delete operations by model.',
+    ['model'])
+
+
+def ExportModelOperationsMixin(model_name):
+    """Returns a mixin for models to export counters for lifecycle operations.
+
+    Usage:
+      class User(ExportModelOperationsMixin('user'), Model):
+          ...
+    """
+    # Force create the labels for this model in the counters. This
+    # is not necessary but it avoids gaps in the aggregated data.
+    model_inserts.labels(model_name)
+    model_updates.labels(model_name)
+    model_deletes.labels(model_name)
+
+    class Mixin(object):
+        def _do_insert(self, *args, **kwargs):
+            model_inserts.labels(model_name).inc()
+            return super(Mixin, self)._do_insert(*args, **kwargs)
+
+        def _do_update(self, *args, **kwargs):
+            model_updates.labels(model_name).inc()
+            return super(Mixin, self)._do_update(*args, **kwargs)
+
+        def delete(self, *args, **kwargs):
+            model_deletes.labels(model_name).inc()
+            return super(Mixin, self).delete(*args, **kwargs)
+    return Mixin

+ 0 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/tests/__init__.py


+ 24 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/tests/test_django_prometheus.py

@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+from django_prometheus.utils import PowersOf
+import unittest
+
+
+class DjangoPrometheusTest(unittest.TestCase):
+    def testPowersOf(self):
+        """Tests utils.PowersOf."""
+        self.assertEqual(
+            [0, 1, 2, 4, 8],
+            PowersOf(2, 4))
+        self.assertEqual(
+            [0, 3, 9, 27, 81, 243],
+            PowersOf(3, 5, lower=1))
+        self.assertEqual(
+            [1, 2, 4, 8],
+            PowersOf(2, 4, include_zero=False))
+        self.assertEqual(
+            [4, 8, 16, 32, 64, 128],
+            PowersOf(2, 6, lower=2, include_zero=False))
+
+
+if __name__ == 'main':
+    unittest.main()

+ 25 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/tests/test_exports.py

@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+import socket
+import unittest
+
+from mock import patch, call, ANY, MagicMock
+
+from django_prometheus.exports import SetupPrometheusEndpointOnPortRange
+
+
+class ExportTest(unittest.TestCase):
+    @patch('django_prometheus.exports.HTTPServer')
+    def testPortRange(self, httpserver_mock):
+        httpserver_mock.side_effect = [socket.error, MagicMock()]
+        port_range = [8000, 8001]
+        SetupPrometheusEndpointOnPortRange(port_range)
+
+        expected_calls = [
+            call(('', 8000), ANY),
+            call(('', 8001), ANY),
+        ]
+        self.assertEqual(httpserver_mock.mock_calls, expected_calls)
+
+
+if __name__ == 'main':
+    unittest.main()

+ 150 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/tests/test_testutils.py

@@ -0,0 +1,150 @@
+#!/usr/bin/env python
+from django_prometheus.testutils import PrometheusTestCaseMixin
+import prometheus_client
+from operator import itemgetter
+import unittest
+
+
+class SomeTestCase(PrometheusTestCaseMixin):
+    """A class that pretends to be a unit test."""
+
+    def __init__(self):
+        self.passes = True
+        super(SomeTestCase, self).__init__()
+
+    def assertEqual(self, l, r, *args, **kwargs):
+        self.passes = self.passes and (l == r)
+
+    def assertFalse(self, e, *args, **kwargs):
+        self.passes = self.passes and (not e)
+
+
+class PrometheusTestCaseMixinTest(unittest.TestCase):
+
+    def setUp(self):
+        self.registry = prometheus_client.CollectorRegistry()
+        self.some_gauge = prometheus_client.Gauge(
+            'some_gauge', 'Some gauge.', registry=self.registry)
+        self.some_gauge.set(42)
+        self.some_labelled_gauge = prometheus_client.Gauge(
+            'some_labelled_gauge', 'Some labelled gauge.', [
+                'labelred', 'labelblue'], registry=self.registry)
+        self.some_labelled_gauge.labels('pink', 'indigo').set(1)
+        self.some_labelled_gauge.labels('pink', 'royal').set(2)
+        self.some_labelled_gauge.labels('carmin', 'indigo').set(3)
+        self.some_labelled_gauge.labels('carmin', 'royal').set(4)
+        self.t = SomeTestCase()
+
+    def testGetMetrics(self):
+        """Tests getMetric."""
+        self.assertEqual(42, self.t.getMetric(
+            'some_gauge', registry=self.registry))
+        self.assertEqual(
+            1,
+            self.t.getMetric(
+                'some_labelled_gauge',
+                registry=self.registry,
+                labelred='pink',
+                labelblue='indigo'))
+
+    def testGetMetricVector(self):
+        """Tests getMetricVector."""
+        vector = self.t.getMetricVector(
+            'some_nonexistent_gauge', registry=self.registry)
+        self.assertEqual([], vector)
+        vector = self.t.getMetricVector('some_gauge', registry=self.registry)
+        self.assertEqual([({}, 42)], vector)
+        vector = self.t.getMetricVector(
+            'some_labelled_gauge', registry=self.registry)
+        self.assertEqual(sorted([
+            ({'labelred': u'pink', 'labelblue': u'indigo'}, 1),
+            ({'labelred': u'pink', 'labelblue': u'royal'}, 2),
+            ({'labelred': u'carmin', 'labelblue': u'indigo'}, 3),
+            ({'labelred': u'carmin', 'labelblue': u'royal'}, 4),
+        ], key=itemgetter(1)), sorted(vector, key=itemgetter(1)))
+
+    def testAssertMetricEquals(self):
+        """Tests assertMetricEquals."""
+        # First we test that a scalar metric can be tested.
+        self.t.assertMetricEquals(42, 'some_gauge', registry=self.registry)
+        self.assertTrue(self.t.passes)
+        self.t.assertMetricEquals(43, 'some_gauge', registry=self.registry)
+        self.assertFalse(self.t.passes)
+        self.t.passes = True
+
+        # Here we test that assertMetricEquals fails on nonexistent gauges.
+        self.t.assertMetricEquals(
+            42, 'some_nonexistent_gauge', registry=self.registry)
+        self.assertFalse(self.t.passes)
+        self.t.passes = True
+
+        # Here we test that labelled metrics can be tested.
+        self.t.assertMetricEquals(
+            1,
+            'some_labelled_gauge',
+            registry=self.registry,
+            labelred='pink',
+            labelblue='indigo')
+        self.assertTrue(self.t.passes)
+        self.t.assertMetricEquals(
+            1,
+            'some_labelled_gauge',
+            registry=self.registry,
+            labelred='tomato',
+            labelblue='sky')
+        self.assertFalse(self.t.passes)
+        self.t.passes = True
+
+    def testRegistrySaving(self):
+        """Tests saveRegistry and frozen registries operations."""
+        frozen_registry = self.t.saveRegistry(registry=self.registry)
+        # Test that we can manipulate a frozen scalar metric.
+        self.assertEqual(42, self.t.getMetricFromFrozenRegistry(
+            'some_gauge', frozen_registry))
+        self.some_gauge.set(99)
+        self.assertEqual(42, self.t.getMetricFromFrozenRegistry(
+            'some_gauge', frozen_registry))
+        self.t.assertMetricDiff(frozen_registry, 99 -
+                                42, 'some_gauge', registry=self.registry)
+        self.assertTrue(self.t.passes)
+        self.t.assertMetricDiff(
+            frozen_registry, 1, 'some_gauge', registry=self.registry)
+        self.assertFalse(self.t.passes)
+        self.t.passes = True
+
+        # Now test the same thing with a labelled metric.
+        self.assertEqual(
+            1,
+            self.t.getMetricFromFrozenRegistry(
+                'some_labelled_gauge',
+                frozen_registry,
+                labelred='pink',
+                labelblue='indigo'))
+        self.some_labelled_gauge.labels('pink', 'indigo').set(5)
+        self.assertEqual(
+            1,
+            self.t.getMetricFromFrozenRegistry(
+                'some_labelled_gauge',
+                frozen_registry,
+                labelred='pink',
+                labelblue='indigo'))
+        self.t.assertMetricDiff(
+            frozen_registry,
+            5 - 1,
+            'some_labelled_gauge',
+            registry=self.registry,
+            labelred='pink',
+            labelblue='indigo')
+        self.assertTrue(self.t.passes)
+        self.t.assertMetricDiff(
+            frozen_registry,
+            1,
+            'some_labelled_gauge',
+            registry=self.registry,
+            labelred='pink',
+            labelblue='indigo')
+        self.assertFalse(self.t.passes)
+
+
+if __name__ == 'main':
+    unittest.main()

+ 156 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/testutils.py

@@ -0,0 +1,156 @@
+import copy
+from prometheus_client import REGISTRY
+
+
+METRIC_EQUALS_ERR_EXPLANATION = """
+%s%s = %s, expected %s.
+The values for %s are:
+%s"""
+
+METRIC_DIFF_ERR_EXPLANATION = """
+%s%s changed by %f, expected %f.
+Value before: %s
+Value after: %s
+"""
+
+METRIC_COMPARE_ERR_EXPLANATION = """
+The change in value of %s%s didn't match the predicate.
+Value before: %s
+Value after: %s
+"""
+
+METRIC_DIFF_ERR_NONE_EXPLANATION = """
+%s%s was None after.
+Value before: %s
+Value after: %s
+"""
+
+
+class PrometheusTestCaseMixin(object):
+    """A collection of utilities that make it easier to write test cases
+    that interact with metrics.
+    """
+
+    def saveRegistry(self, registry=REGISTRY):
+        """Freezes a registry. This lets a user test changes to a metric
+        instead of testing the absolute value. A typical use case looks like:
+
+          r = self.saveRegistry()
+          doStuff()
+          self.assertMetricDiff(r, 1, 'stuff_done_total')
+        """
+        return copy.deepcopy(list(registry.collect()))
+
+    def getMetricFromFrozenRegistry(self, metric_name, frozen_registry,
+                                    **labels):
+        """Gets a single metric from a frozen registry."""
+        for metric in frozen_registry:
+            for n, l, value in metric.samples:
+                if n == metric_name and l == labels:
+                    return value
+
+    def getMetric(self, metric_name, registry=REGISTRY, **labels):
+        """Gets a single metric."""
+        return self.getMetricFromFrozenRegistry(
+            metric_name, registry.collect(), **labels)
+
+    def getMetricVectorFromFrozenRegistry(self, metric_name, frozen_registry):
+        """Like getMetricVector, but from a frozen registry."""
+        output = []
+        for metric in frozen_registry:
+            for n, l, value in metric.samples:
+                if n == metric_name:
+                    output.append((l, value))
+        return output
+
+    def getMetricVector(self, metric_name, registry=REGISTRY):
+        """Returns the values for all labels of a given metric.
+
+        The result is returned as a list of (labels, value) tuples,
+        where `labels` is a dict.
+
+        This is quite a hack since it relies on the internal
+        representation of the prometheus_client, and it should
+        probably be provided as a function there instead.
+        """
+        return self.getMetricVectorFromFrozenRegistry(
+            metric_name, registry.collect())
+
+    def formatLabels(self, labels):
+        """Format a set of labels to Prometheus representation.
+
+        In:
+          {'method': 'GET', 'port': '80'}
+
+        Out:
+          '{method="GET",port="80"}'
+        """
+        return '{%s}' % ','.join([
+            '%s="%s"' % (k, v) for k, v in labels.items()])
+
+    def formatVector(self, vector):
+        """Formats a list of (labels, value) where labels is a dict into a
+        human-readable representation.
+        """
+        return '\n'.join([
+            '%s = %s' % (self.formatLabels(labels), value)
+            for labels, value in vector])
+
+    def assertMetricEquals(self, expected_value, metric_name,
+                           registry=REGISTRY, **labels):
+        """Asserts that metric_name{**labels} == expected_value."""
+        value = self.getMetric(metric_name, registry=registry, **labels)
+        self.assertEqual(
+            expected_value, value, METRIC_EQUALS_ERR_EXPLANATION % (
+                metric_name, self.formatLabels(labels), value,
+                expected_value, metric_name,
+                self.formatVector(self.getMetricVector(metric_name))))
+
+    def assertMetricDiff(self, frozen_registry, expected_diff,
+                         metric_name, registry=REGISTRY, **labels):
+        """Asserts that metric_name{**labels} changed by expected_diff between
+        the frozen registry and now. A frozen registry can be obtained
+        by calling saveRegistry, typically at the beginning of a test
+        case.
+        """
+        saved_value = self.getMetricFromFrozenRegistry(
+            metric_name, frozen_registry, **labels)
+        current_value = self.getMetric(metric_name, registry=registry,
+                                       **labels)
+        self.assertFalse(
+            current_value is None,
+            METRIC_DIFF_ERR_NONE_EXPLANATION % (
+                metric_name, self.formatLabels(labels),
+                saved_value,
+                current_value))
+        diff = current_value - (saved_value or 0.0)
+        self.assertEqual(
+            expected_diff, diff,
+            METRIC_DIFF_ERR_EXPLANATION % (
+                metric_name, self.formatLabels(labels), diff, expected_diff,
+                saved_value,
+                current_value))
+
+    def assertMetricCompare(self, frozen_registry, predicate,
+                            metric_name, registry=REGISTRY, **labels):
+        """Asserts that metric_name{**labels} changed according to a provided
+        predicate function between the frozen registry and now. A
+        frozen registry can be obtained by calling saveRegistry,
+        typically at the beginning of a test case.
+        """
+        saved_value = self.getMetricFromFrozenRegistry(
+            metric_name, frozen_registry, **labels)
+        current_value = self.getMetric(metric_name, registry=registry,
+                                       **labels)
+        self.assertFalse(
+            current_value is None,
+            METRIC_DIFF_ERR_NONE_EXPLANATION % (
+                metric_name, self.formatLabels(labels),
+                saved_value,
+                current_value))
+        self.assertTrue(
+            predicate(saved_value, current_value),
+            METRIC_COMPARE_ERR_EXPLANATION % (
+                metric_name, self.formatLabels(labels),
+                saved_value,
+                current_value))

+ 7 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/urls.py

@@ -0,0 +1,7 @@
+from django.conf.urls import url
+from django_prometheus import exports
+
+urlpatterns = [
+    url(r'^metrics$', exports.ExportToDjangoView,
+        name='prometheus-django-metrics'),
+]

+ 29 - 0
desktop/core/ext-py/django-prometheus-1.0.15/django_prometheus/utils.py

@@ -0,0 +1,29 @@
+from timeit import default_timer
+
+
+def Time():
+    """Returns some representation of the current time.
+
+    This wrapper is meant to take advantage of a higher time
+    resolution when available. Thus, its return value should be
+    treated as an opaque object. It can be compared to the current
+    time with TimeSince().
+    """
+    return default_timer()
+
+
+def TimeSince(t):
+    """Compares a value returned by Time() to the current time.
+
+    Returns:
+      the time since t, in fractional seconds.
+    """
+    return default_timer() - t
+
+
+def PowersOf(logbase, count, lower=0, include_zero=True):
+    """Returns a list of count powers of logbase (from logbase**lower)."""
+    if not include_zero:
+        return [logbase ** i for i in range(lower, count+lower)]
+    else:
+        return [0] + [logbase ** i for i in range(lower, count+lower)]

+ 4 - 0
desktop/core/ext-py/django-prometheus-1.0.15/setup.cfg

@@ -0,0 +1,4 @@
+[egg_info]
+tag_build = 
+tag_date = 0
+

+ 44 - 0
desktop/core/ext-py/django-prometheus-1.0.15/setup.py

@@ -0,0 +1,44 @@
+import os
+from setuptools import setup, find_packages
+
+LONG_DESCRIPTION = """Django-Prometheus
+
+This library contains code to expose some monitoring metrics relevant
+to Django internals so they can be monitored by Prometheus.io.
+
+See https://github.com/korfuri/django-prometheus for usage
+instructions.
+"""
+
+setup(
+    name="django-prometheus",
+    version="1.0.15",
+    author="Uriel Corfa",
+    author_email="uriel@corfa.fr",
+    description=(
+        "Django middlewares to monitor your application with Prometheus.io."),
+    license="Apache",
+    keywords="django monitoring prometheus",
+    url="http://github.com/korfuri/django-prometheus",
+    packages=find_packages(),
+    test_suite="django_prometheus.tests",
+    long_description=LONG_DESCRIPTION,
+    install_requires=[
+        "prometheus_client>=0.0.21",
+    ],
+    classifiers=[
+        "Development Status :: 5 - Production/Stable",
+        "Intended Audience :: Developers",
+        "Intended Audience :: Information Technology",
+        "Intended Audience :: System Administrators",
+        "Programming Language :: Python :: 2",
+        "Programming Language :: Python :: 2.7",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.3",
+        "Programming Language :: Python :: 3.4",
+        "Programming Language :: Python :: 3.5",
+        "Framework :: Django",
+        "Topic :: System :: Monitoring",
+        "License :: OSI Approved :: Apache Software License",
+    ],
+)

+ 29 - 0
desktop/core/ext-py/prometheus_client-0.7.1/PKG-INFO

@@ -0,0 +1,29 @@
+Metadata-Version: 1.1
+Name: prometheus_client
+Version: 0.7.1
+Summary: Python client for the Prometheus monitoring system.
+Home-page: https://github.com/prometheus/client_python
+Author: Brian Brazil
+Author-email: brian.brazil@robustperception.io
+License: Apache Software License 2.0
+Description-Content-Type: UNKNOWN
+Description: See https://github.com/prometheus/client_python/blob/master/README.md for documentation.
+Keywords: prometheus monitoring instrumentation client
+Platform: UNKNOWN
+Classifier: Development Status :: 4 - Beta
+Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: Information Technology
+Classifier: Intended Audience :: System Administrators
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Topic :: System :: Monitoring
+Classifier: License :: OSI Approved :: Apache Software License

+ 542 - 0
desktop/core/ext-py/prometheus_client-0.7.1/README.md

@@ -0,0 +1,542 @@
+# Prometheus Python Client
+
+The official Python 2 and 3 client for [Prometheus](http://prometheus.io).
+
+## Three Step Demo
+
+**One**: Install the client:
+```
+pip install prometheus_client
+```
+
+**Two**: Paste the following into a Python interpreter:
+```python
+from prometheus_client import start_http_server, Summary
+import random
+import time
+
+# Create a metric to track time spent and requests made.
+REQUEST_TIME = Summary('request_processing_seconds', 'Time spent processing request')
+
+# Decorate function with metric.
+@REQUEST_TIME.time()
+def process_request(t):
+    """A dummy function that takes some time."""
+    time.sleep(t)
+
+if __name__ == '__main__':
+    # Start up the server to expose the metrics.
+    start_http_server(8000)
+    # Generate some requests.
+    while True:
+        process_request(random.random())
+```
+
+**Three**: Visit [http://localhost:8000/](http://localhost:8000/) to view the metrics.
+
+From one easy to use decorator you get:
+  * `request_processing_seconds_count`: Number of times this function was called.
+  * `request_processing_seconds_sum`: Total amount of time spent in this function.
+
+Prometheus's `rate` function allows calculation of both requests per second,
+and latency over time from this data.
+
+In addition if you're on Linux the `process` metrics expose CPU, memory and
+other information about the process for free!
+
+## Installation
+
+```
+pip install prometheus_client
+```
+
+This package can be found on
+[PyPI](https://pypi.python.org/pypi/prometheus_client).
+
+## Instrumenting
+
+Four types of metric are offered: Counter, Gauge, Summary and Histogram.
+See the documentation on [metric types](http://prometheus.io/docs/concepts/metric_types/)
+and [instrumentation best practices](https://prometheus.io/docs/practices/instrumentation/#counter-vs-gauge-summary-vs-histogram)
+on how to use them.
+
+### Counter
+
+Counters go up, and reset when the process restarts.
+
+
+```python
+from prometheus_client import Counter
+c = Counter('my_failures', 'Description of counter')
+c.inc()     # Increment by 1
+c.inc(1.6)  # Increment by given value
+```
+
+If there is a suffix of `_total` on the metric name, it will be removed. When
+exposing the time series for counter, a `_total` suffix will be added. This is
+for compatibility between OpenMetrics and the Prometheus text format, as OpenMetrics
+requires the `_total` suffix.
+
+There are utilities to count exceptions raised:
+
+```python
+@c.count_exceptions()
+def f():
+  pass
+
+with c.count_exceptions():
+  pass
+
+# Count only one type of exception
+with c.count_exceptions(ValueError):
+  pass
+```
+
+### Gauge
+
+Gauges can go up and down.
+
+```python
+from prometheus_client import Gauge
+g = Gauge('my_inprogress_requests', 'Description of gauge')
+g.inc()      # Increment by 1
+g.dec(10)    # Decrement by given value
+g.set(4.2)   # Set to a given value
+```
+
+There are utilities for common use cases:
+
+```python
+g.set_to_current_time()   # Set to current unixtime
+
+# Increment when entered, decrement when exited.
+@g.track_inprogress()
+def f():
+  pass
+
+with g.track_inprogress():
+  pass
+```
+
+A Gauge can also take its value from a callback:
+
+```python
+d = Gauge('data_objects', 'Number of objects')
+my_dict = {}
+d.set_function(lambda: len(my_dict))
+```
+
+### Summary
+
+Summaries track the size and number of events.
+
+```python
+from prometheus_client import Summary
+s = Summary('request_latency_seconds', 'Description of summary')
+s.observe(4.7)    # Observe 4.7 (seconds in this case)
+```
+
+There are utilities for timing code:
+
+```python
+@s.time()
+def f():
+  pass
+
+with s.time():
+  pass
+```
+
+The Python client doesn't store or expose quantile information at this time.
+
+### Histogram
+
+Histograms track the size and number of events in buckets.
+This allows for aggregatable calculation of quantiles.
+
+```python
+from prometheus_client import Histogram
+h = Histogram('request_latency_seconds', 'Description of histogram')
+h.observe(4.7)    # Observe 4.7 (seconds in this case)
+```
+
+The default buckets are intended to cover a typical web/rpc request from milliseconds to seconds.
+They can be overridden by passing `buckets` keyword argument to `Histogram`.
+
+There are utilities for timing code:
+
+```python
+@h.time()
+def f():
+  pass
+
+with h.time():
+  pass
+```
+
+### Info
+
+Info tracks key-value information, usually about a whole target.
+
+```python
+from prometheus_client import Info
+i = Info('my_build_version', 'Description of info')
+i.info({'version': '1.2.3', 'buildhost': 'foo@bar'})
+```
+
+### Enum
+
+Enum tracks which of a set of states something is currently in.
+
+```python
+from prometheus_client import Enum
+e = Enum('my_task_state', 'Description of enum',
+        states=['starting', 'running', 'stopped'])
+e.state('running')
+```
+
+### Labels
+
+All metrics can have labels, allowing grouping of related time series.
+
+See the best practices on [naming](http://prometheus.io/docs/practices/naming/)
+and [labels](http://prometheus.io/docs/practices/instrumentation/#use-labels).
+
+Taking a counter as an example:
+
+```python
+from prometheus_client import Counter
+c = Counter('my_requests_total', 'HTTP Failures', ['method', 'endpoint'])
+c.labels('get', '/').inc()
+c.labels('post', '/submit').inc()
+```
+
+Labels can also be passed as keyword-arguments:
+
+```python
+from prometheus_client import Counter
+c = Counter('my_requests_total', 'HTTP Failures', ['method', 'endpoint'])
+c.labels(method='get', endpoint='/').inc()
+c.labels(method='post', endpoint='/submit').inc()
+```
+
+### Process Collector
+
+The Python client automatically exports metrics about process CPU usage, RAM,
+file descriptors and start time. These all have the prefix `process`, and
+are only currently available on Linux.
+
+The namespace and pid constructor arguments allows for exporting metrics about
+other processes, for example:
+```
+ProcessCollector(namespace='mydaemon', pid=lambda: open('/var/run/daemon.pid').read())
+```
+
+### Platform Collector
+
+The client also automatically exports some metadata about Python. If using Jython,
+metadata about the JVM in use is also included. This information is available as 
+labels on the `python_info` metric. The value of the metric is 1, since it is the 
+labels that carry information.
+
+## Exporting
+
+There are several options for exporting metrics.
+
+### HTTP
+
+Metrics are usually exposed over HTTP, to be read by the Prometheus server.
+
+The easiest way to do this is via `start_http_server`, which will start a HTTP
+server in a daemon thread on the given port:
+
+```python
+from prometheus_client import start_http_server
+
+start_http_server(8000)
+```
+
+Visit [http://localhost:8000/](http://localhost:8000/) to view the metrics.
+
+To add Prometheus exposition to an existing HTTP server, see the `MetricsHandler` class
+which provides a `BaseHTTPRequestHandler`. It also serves as a simple example of how
+to write a custom endpoint.
+
+#### Twisted
+
+To use prometheus with [twisted](https://twistedmatrix.com/), there is `MetricsResource` which exposes metrics as a twisted resource.
+
+```python
+from prometheus_client.twisted import MetricsResource
+from twisted.web.server import Site
+from twisted.web.resource import Resource
+from twisted.internet import reactor
+
+root = Resource()
+root.putChild(b'metrics', MetricsResource())
+
+factory = Site(root)
+reactor.listenTCP(8000, factory)
+reactor.run()
+```
+
+#### WSGI
+
+To use Prometheus with [WSGI](http://wsgi.readthedocs.org/en/latest/), there is
+`make_wsgi_app` which creates a WSGI application.
+
+```python
+from prometheus_client import make_wsgi_app
+from wsgiref.simple_server import make_server
+
+app = make_wsgi_app()
+httpd = make_server('', 8000, app)
+httpd.serve_forever()
+```
+
+Such an application can be useful when integrating Prometheus metrics with WSGI
+apps.
+
+The method `start_wsgi_server` can be used to serve the metrics through the
+WSGI reference implementation in a new thread.
+
+```python
+from prometheus_client import start_wsgi_server
+
+start_wsgi_server(8000)
+```
+
+#### Flask
+
+To use Prometheus with [Flask](http://flask.pocoo.org/) we need to serve metrics through a Prometheus WSGI application. This can be achieved using [Flask's application dispatching](http://flask.pocoo.org/docs/latest/patterns/appdispatch/). Below is a working example.
+
+Save the snippet below in a `myapp.py` file
+
+```python
+from flask import Flask
+from werkzeug.wsgi import DispatcherMiddleware
+from prometheus_client import make_wsgi_app
+
+# Create my app
+app = Flask(__name__)
+
+# Add prometheus wsgi middleware to route /metrics requests
+app_dispatch = DispatcherMiddleware(app, {
+    '/metrics': make_wsgi_app()
+})
+```
+
+Run the example web application like this
+
+```bash
+# Install uwsgi if you do not have it
+pip install uwsgi
+uwsgi --http 127.0.0.1:8000 --wsgi-file myapp.py --callable app_dispatch
+```
+
+Visit http://localhost:8000/metrics to see the metrics
+
+### Node exporter textfile collector
+
+The [textfile collector](https://github.com/prometheus/node_exporter#textfile-collector)
+allows machine-level statistics to be exported out via the Node exporter.
+
+This is useful for monitoring cronjobs, or for writing cronjobs to expose metrics
+about a machine system that the Node exporter does not support or would not make sense
+to perform at every scrape (for example, anything involving subprocesses).
+
+```python
+from prometheus_client import CollectorRegistry, Gauge, write_to_textfile
+
+registry = CollectorRegistry()
+g = Gauge('raid_status', '1 if raid array is okay', registry=registry)
+g.set(1)
+write_to_textfile('/configured/textfile/path/raid.prom', registry)
+```
+
+A separate registry is used, as the default registry may contain other metrics
+such as those from the Process Collector.
+
+## Exporting to a Pushgateway
+
+The [Pushgateway](https://github.com/prometheus/pushgateway)
+allows ephemeral and batch jobs to expose their metrics to Prometheus.
+
+```python
+from prometheus_client import CollectorRegistry, Gauge, push_to_gateway
+
+registry = CollectorRegistry()
+g = Gauge('job_last_success_unixtime', 'Last time a batch job successfully finished', registry=registry)
+g.set_to_current_time()
+push_to_gateway('localhost:9091', job='batchA', registry=registry)
+```
+
+A separate registry is used, as the default registry may contain other metrics
+such as those from the Process Collector.
+
+Pushgateway functions take a grouping key. `push_to_gateway` replaces metrics
+with the same grouping key, `pushadd_to_gateway` only replaces metrics with the
+same name and grouping key and `delete_from_gateway` deletes metrics with the
+given job and grouping key. See the
+[Pushgateway documentation](https://github.com/prometheus/pushgateway/blob/master/README.md)
+for more information.
+
+`instance_ip_grouping_key` returns a grouping key with the instance label set
+to the host's IP address.
+
+### Handlers for authentication
+
+If the push gateway you are connecting to is protected with HTTP Basic Auth,
+you can use a special handler to set the Authorization header.
+
+```python
+from prometheus_client import CollectorRegistry, Gauge, push_to_gateway
+from prometheus_client.exposition import basic_auth_handler
+
+def my_auth_handler(url, method, timeout, headers, data):
+    username = 'foobar'
+    password = 'secret123'
+    return basic_auth_handler(url, method, timeout, headers, data, username, password)
+registry = CollectorRegistry()
+g = Gauge('job_last_success_unixtime', 'Last time a batch job successfully finished', registry=registry)
+g.set_to_current_time()
+push_to_gateway('localhost:9091', job='batchA', registry=registry, handler=my_auth_handler)
+```
+
+## Bridges
+
+It is also possible to expose metrics to systems other than Prometheus.
+This allows you to take advantage of Prometheus instrumentation even
+if you are not quite ready to fully transition to Prometheus yet.
+
+### Graphite
+
+Metrics are pushed over TCP in the Graphite plaintext format.
+
+```python
+from prometheus_client.bridge.graphite import GraphiteBridge
+
+gb = GraphiteBridge(('graphite.your.org', 2003))
+# Push once.
+gb.push()
+# Push every 10 seconds in a daemon thread.
+gb.start(10.0)
+```
+
+## Custom Collectors
+
+Sometimes it is not possible to directly instrument code, as it is not
+in your control. This requires you to proxy metrics from other systems.
+
+To do so you need to create a custom collector, for example:
+
+```python
+from prometheus_client.core import GaugeMetricFamily, CounterMetricFamily, REGISTRY
+
+class CustomCollector(object):
+    def collect(self):
+        yield GaugeMetricFamily('my_gauge', 'Help text', value=7)
+        c = CounterMetricFamily('my_counter_total', 'Help text', labels=['foo'])
+        c.add_metric(['bar'], 1.7)
+        c.add_metric(['baz'], 3.8)
+        yield c
+
+REGISTRY.register(CustomCollector())
+```
+
+`SummaryMetricFamily` and `HistogramMetricFamily` work similarly.
+
+A collector may implement a `describe` method which returns metrics in the same
+format as `collect` (though you don't have to include the samples). This is
+used to predetermine the names of time series a `CollectorRegistry` exposes and
+thus to detect collisions and duplicate registrations.
+
+Usually custom collectors do not have to implement `describe`. If `describe` is
+not implemented and the CollectorRegistry was created with `auto_desribe=True`
+(which is the case for the default registry) then `collect` will be called at
+registration time instead of `describe`. If this could cause problems, either
+implement a proper `describe`, or if that's not practical have `describe`
+return an empty list.
+
+
+## Multiprocess Mode (Gunicorn)
+
+Prometheus client libraries presume a threaded model, where metrics are shared
+across workers. This doesn't work so well for languages such as Python where
+it's common to have processes rather than threads to handle large workloads.
+
+To handle this the client library can be put in multiprocess mode.
+This comes with a number of limitations:
+
+- Registries can not be used as normal, all instantiated metrics are exported
+- Custom collectors do not work (e.g. cpu and memory metrics)
+- Info and Enum metrics do not work
+- The pushgateway cannot be used
+- Gauges cannot use the `pid` label
+
+There's several steps to getting this working:
+
+**One**: Gunicorn deployment
+
+The `prometheus_multiproc_dir` environment variable must be set to a directory
+that the client library can use for metrics. This directory must be wiped
+between Gunicorn runs (before startup is recommended).
+
+Put the following in the config file:
+```python
+from prometheus_client import multiprocess
+
+def child_exit(server, worker):
+    multiprocess.mark_process_dead(worker.pid)
+```
+
+**Two**: Inside the application
+```python
+from prometheus_client import multiprocess
+from prometheus_client import generate_latest, CollectorRegistry, CONTENT_TYPE_LATEST, Gauge
+
+# Example gauge.
+IN_PROGRESS = Gauge("inprogress_requests", "help", multiprocess_mode='livesum')
+
+
+# Expose metrics.
+@IN_PROGRESS.track_inprogress()
+def app(environ, start_response):
+    registry = CollectorRegistry()
+    multiprocess.MultiProcessCollector(registry)
+    data = generate_latest(registry)
+    status = '200 OK'
+    response_headers = [
+        ('Content-type', CONTENT_TYPE_LATEST),
+        ('Content-Length', str(len(data)))
+    ]
+    start_response(status, response_headers)
+    return iter([data])
+```
+
+**Three**: Instrumentation
+
+Counters, Summarys and Histograms work as normal.
+
+Gauges have several modes they can run in, which can be selected with the
+`multiprocess_mode` parameter.
+
+- 'all': Default. Return a timeseries per process alive or dead.
+- 'liveall': Return a timeseries per process that is still alive.
+- 'livesum': Return a single timeseries that is the sum of the values of alive processes.
+- 'max': Return a single timeseries that is the maximum of the values of all processes, alive or dead.
+- 'min': Return a single timeseries that is the minimum of the values of all processes, alive or dead.
+
+## Parser
+
+The Python client supports parsing the Prometheus text format.
+This is intended for advanced use cases where you have servers
+exposing Prometheus metrics and need to get them into some other
+system.
+
+```python
+from prometheus_client.parser import text_string_to_metric_families
+for family in text_string_to_metric_families(u"my_gauge 1.0\n"):
+  for sample in family.samples:
+    print("Name: {0} Labels: {1} Value: {2}".format(*sample))
+```

+ 61 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/__init__.py

@@ -0,0 +1,61 @@
+#!/usr/bin/python
+
+from . import exposition
+from . import gc_collector
+from . import metrics
+from . import metrics_core
+from . import platform_collector
+from . import process_collector
+from . import registry
+
+__all__ = ['Counter', 'Gauge', 'Summary', 'Histogram', 'Info', 'Enum']
+
+CollectorRegistry = registry.CollectorRegistry
+REGISTRY = registry.REGISTRY
+Metric = metrics_core.Metric
+Counter = metrics.Counter
+Gauge = metrics.Gauge
+Summary = metrics.Summary
+Histogram = metrics.Histogram
+Info = metrics.Info
+Enum = metrics.Enum
+
+CONTENT_TYPE_LATEST = exposition.CONTENT_TYPE_LATEST
+generate_latest = exposition.generate_latest
+MetricsHandler = exposition.MetricsHandler
+make_wsgi_app = exposition.make_wsgi_app
+start_http_server = exposition.start_http_server
+start_wsgi_server = exposition.start_wsgi_server
+write_to_textfile = exposition.write_to_textfile
+push_to_gateway = exposition.push_to_gateway
+pushadd_to_gateway = exposition.pushadd_to_gateway
+delete_from_gateway = exposition.delete_from_gateway
+instance_ip_grouping_key = exposition.instance_ip_grouping_key
+
+ProcessCollector = process_collector.ProcessCollector
+PROCESS_COLLECTOR = process_collector.PROCESS_COLLECTOR
+
+PlatformCollector = platform_collector.PlatformCollector
+PLATFORM_COLLECTOR = platform_collector.PLATFORM_COLLECTOR
+
+GCCollector = gc_collector.GCCollector
+GC_COLLECTOR = gc_collector.GC_COLLECTOR
+
+if __name__ == '__main__':
+    c = Counter('cc', 'A counter')
+    c.inc()
+
+    g = Gauge('gg', 'A gauge')
+    g.set(17)
+
+    s = Summary('ss', 'A summary', ['a', 'b'])
+    s.labels('c', 'd').observe(17)
+
+    h = Histogram('hh', 'A histogram')
+    h.observe(.6)
+
+    start_http_server(8000)
+    import time
+
+    while True:
+        time.sleep(1)

+ 0 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/bridge/__init__.py


+ 82 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/bridge/graphite.py

@@ -0,0 +1,82 @@
+#!/usr/bin/python
+from __future__ import unicode_literals
+
+import logging
+import re
+import socket
+import threading
+import time
+from timeit import default_timer
+
+from ..registry import REGISTRY
+
+# Roughly, have to keep to what works as a file name.
+# We also remove periods, so labels can be distinguished.
+
+_INVALID_GRAPHITE_CHARS = re.compile(r"[^a-zA-Z0-9_-]")
+
+
+def _sanitize(s):
+    return _INVALID_GRAPHITE_CHARS.sub('_', s)
+
+
+class _RegularPush(threading.Thread):
+    def __init__(self, pusher, interval, prefix):
+        super(_RegularPush, self).__init__()
+        self._pusher = pusher
+        self._interval = interval
+        self._prefix = prefix
+
+    def run(self):
+        wait_until = default_timer()
+        while True:
+            while True:
+                now = default_timer()
+                if now >= wait_until:
+                    # May need to skip some pushes.
+                    while wait_until < now:
+                        wait_until += self._interval
+                    break
+                # time.sleep can return early.
+                time.sleep(wait_until - now)
+            try:
+                self._pusher.push(prefix=self._prefix)
+            except IOError:
+                logging.exception("Push failed")
+
+
+class GraphiteBridge(object):
+    def __init__(self, address, registry=REGISTRY, timeout_seconds=30, _timer=time.time):
+        self._address = address
+        self._registry = registry
+        self._timeout = timeout_seconds
+        self._timer = _timer
+
+    def push(self, prefix=''):
+        now = int(self._timer())
+        output = []
+
+        prefixstr = ''
+        if prefix:
+            prefixstr = prefix + '.'
+
+        for metric in self._registry.collect():
+            for s in metric.samples:
+                if s.labels:
+                    labelstr = '.' + '.'.join(
+                        ['{0}.{1}'.format(
+                            _sanitize(k), _sanitize(v))
+                            for k, v in sorted(s.labels.items())])
+                else:
+                    labelstr = ''
+                output.append('{0}{1}{2} {3} {4}\n'.format(
+                    prefixstr, _sanitize(s.name), labelstr, float(s.value), now))
+
+        conn = socket.create_connection(self._address, self._timeout)
+        conn.sendall(''.join(output).encode('ascii'))
+        conn.close()
+
+    def start(self, interval=60.0, prefix=''):
+        t = _RegularPush(self, interval, prefix)
+        t.daemon = True
+        t.start()

+ 68 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/context_managers.py

@@ -0,0 +1,68 @@
+from __future__ import unicode_literals
+
+from timeit import default_timer
+
+from .decorator import decorate
+
+
+class ExceptionCounter(object):
+    def __init__(self, counter, exception):
+        self._counter = counter
+        self._exception = exception
+
+    def __enter__(self):
+        pass
+
+    def __exit__(self, typ, value, traceback):
+        if isinstance(value, self._exception):
+            self._counter.inc()
+
+    def __call__(self, f):
+        def wrapped(func, *args, **kwargs):
+            with self:
+                return func(*args, **kwargs)
+
+        return decorate(f, wrapped)
+
+
+class InprogressTracker(object):
+    def __init__(self, gauge):
+        self._gauge = gauge
+
+    def __enter__(self):
+        self._gauge.inc()
+
+    def __exit__(self, typ, value, traceback):
+        self._gauge.dec()
+
+    def __call__(self, f):
+        def wrapped(func, *args, **kwargs):
+            with self:
+                return func(*args, **kwargs)
+
+        return decorate(f, wrapped)
+
+
+class Timer(object):
+    def __init__(self, callback):
+        self._callback = callback
+
+    def _new_timer(self):
+        return self.__class__(self._callback)
+
+    def __enter__(self):
+        self._start = default_timer()
+
+    def __exit__(self, typ, value, traceback):
+        # Time can go backwards.
+        duration = max(default_timer() - self._start, 0)
+        self._callback(duration)
+
+    def __call__(self, f):
+        def wrapped(func, *args, **kwargs):
+            # Obtaining new instance of timer every time
+            # ensures thread safety and reentrancy.
+            with self._new_timer():
+                return func(*args, **kwargs)
+
+        return decorate(f, wrapped)

+ 32 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/core.py

@@ -0,0 +1,32 @@
+from __future__ import unicode_literals
+
+from .metrics import Counter, Enum, Gauge, Histogram, Info, Summary
+from .metrics_core import (
+    CounterMetricFamily, GaugeHistogramMetricFamily, GaugeMetricFamily, HistogramMetricFamily, InfoMetricFamily,
+    Metric, StateSetMetricFamily, SummaryMetricFamily, UnknownMetricFamily, UntypedMetricFamily)
+from .registry import CollectorRegistry, REGISTRY
+from .samples import Exemplar, Sample, Timestamp
+
+__all__ = (
+    'CollectorRegistry',
+    'Counter',
+    'CounterMetricFamily',
+    'Enum',
+    'Exemplar',
+    'Gauge',
+    'GaugeHistogramMetricFamily',
+    'GaugeMetricFamily',
+    'Histogram',
+    'HistogramMetricFamily',
+    'Info',
+    'InfoMetricFamily',
+    'Metric',
+    'REGISTRY',
+    'Sample',
+    'StateSetMetricFamily',
+    'Summary',
+    'SummaryMetricFamily',
+    'Timestamp',
+    'UnknownMetricFamily',
+    'UntypedMetricFamily',
+)

+ 427 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/decorator.py

@@ -0,0 +1,427 @@
+# #########################     LICENSE     ############################ #
+
+# Copyright (c) 2005-2016, Michele Simionato
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+
+#   Redistributions of source code must retain the above copyright
+#   notice, this list of conditions and the following disclaimer.
+#   Redistributions in bytecode form must reproduce the above copyright
+#   notice, this list of conditions and the following disclaimer in
+#   the documentation and/or other materials provided with the
+#   distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+
+"""
+Decorator module, see http://pypi.python.org/pypi/decorator
+for the documentation.
+"""
+from __future__ import print_function
+
+import collections
+import inspect
+import itertools
+import operator
+import re
+import sys
+
+__version__ = '4.0.10'
+
+if sys.version_info >= (3,):
+    from inspect import getfullargspec
+
+
+    def get_init(cls):
+        return cls.__init__
+else:
+    class getfullargspec(object):
+        "A quick and dirty replacement for getfullargspec for Python 2.X"
+
+        def __init__(self, f):
+            self.args, self.varargs, self.varkw, self.defaults = \
+                inspect.getargspec(f)
+            self.kwonlyargs = []
+            self.kwonlydefaults = None
+
+        def __iter__(self):
+            yield self.args
+            yield self.varargs
+            yield self.varkw
+            yield self.defaults
+
+        getargspec = inspect.getargspec
+
+
+    def get_init(cls):
+        return cls.__init__.__func__
+
+# getargspec has been deprecated in Python 3.5
+ArgSpec = collections.namedtuple(
+    'ArgSpec', 'args varargs varkw defaults')
+
+
+def getargspec(f):
+    """A replacement for inspect.getargspec"""
+    spec = getfullargspec(f)
+    return ArgSpec(spec.args, spec.varargs, spec.varkw, spec.defaults)
+
+
+DEF = re.compile(r'\s*def\s*([_\w][_\w\d]*)\s*\(')
+
+
+# basic functionality
+class FunctionMaker(object):
+    """
+    An object with the ability to create functions with a given signature.
+    It has attributes name, doc, module, signature, defaults, dict and
+    methods update and make.
+    """
+
+    # Atomic get-and-increment provided by the GIL
+    _compile_count = itertools.count()
+
+    def __init__(self, func=None, name=None, signature=None,
+                 defaults=None, doc=None, module=None, funcdict=None):
+        self.shortsignature = signature
+        if func:
+            # func can be a class or a callable, but not an instance method
+            self.name = func.__name__
+            if self.name == '<lambda>':  # small hack for lambda functions
+                self.name = '_lambda_'
+            self.doc = func.__doc__
+            self.module = func.__module__
+            if inspect.isfunction(func):
+                argspec = getfullargspec(func)
+                self.annotations = getattr(func, '__annotations__', {})
+                for a in ('args', 'varargs', 'varkw', 'defaults', 'kwonlyargs',
+                          'kwonlydefaults'):
+                    setattr(self, a, getattr(argspec, a))
+                for i, arg in enumerate(self.args):
+                    setattr(self, 'arg%d' % i, arg)
+                if sys.version_info < (3,):  # easy way
+                    self.shortsignature = self.signature = (
+                        inspect.formatargspec(
+                            formatvalue=lambda val: "", *argspec)[1:-1])
+                else:  # Python 3 way
+                    allargs = list(self.args)
+                    allshortargs = list(self.args)
+                    if self.varargs:
+                        allargs.append('*' + self.varargs)
+                        allshortargs.append('*' + self.varargs)
+                    elif self.kwonlyargs:
+                        allargs.append('*')  # single star syntax
+                    for a in self.kwonlyargs:
+                        allargs.append('%s=None' % a)
+                        allshortargs.append('%s=%s' % (a, a))
+                    if self.varkw:
+                        allargs.append('**' + self.varkw)
+                        allshortargs.append('**' + self.varkw)
+                    self.signature = ', '.join(allargs)
+                    self.shortsignature = ', '.join(allshortargs)
+                self.dict = func.__dict__.copy()
+        # func=None happens when decorating a caller
+        if name:
+            self.name = name
+        if signature is not None:
+            self.signature = signature
+        if defaults:
+            self.defaults = defaults
+        if doc:
+            self.doc = doc
+        if module:
+            self.module = module
+        if funcdict:
+            self.dict = funcdict
+        # check existence required attributes
+        assert hasattr(self, 'name')
+        if not hasattr(self, 'signature'):
+            raise TypeError('You are decorating a non function: %s' % func)
+
+    def update(self, func, **kw):
+        "Update the signature of func with the data in self"
+        func.__name__ = self.name
+        func.__doc__ = getattr(self, 'doc', None)
+        func.__dict__ = getattr(self, 'dict', {})
+        func.__defaults__ = getattr(self, 'defaults', ())
+        func.__kwdefaults__ = getattr(self, 'kwonlydefaults', None)
+        func.__annotations__ = getattr(self, 'annotations', None)
+        try:
+            frame = sys._getframe(3)
+        except AttributeError:  # for IronPython and similar implementations
+            callermodule = '?'
+        else:
+            callermodule = frame.f_globals.get('__name__', '?')
+        func.__module__ = getattr(self, 'module', callermodule)
+        func.__dict__.update(kw)
+
+    def make(self, src_templ, evaldict=None, addsource=False, **attrs):
+        "Make a new function from a given template and update the signature"
+        src = src_templ % vars(self)  # expand name and signature
+        evaldict = evaldict or {}
+        mo = DEF.match(src)
+        if mo is None:
+            raise SyntaxError('not a valid function template\n%s' % src)
+        name = mo.group(1)  # extract the function name
+        names = set([name] + [arg.strip(' *') for arg in
+                              self.shortsignature.split(',')])
+        for n in names:
+            if n in ('_func_', '_call_'):
+                raise NameError('%s is overridden in\n%s' % (n, src))
+
+        if not src.endswith('\n'):  # add a newline for old Pythons
+            src += '\n'
+
+        # Ensure each generated function has a unique filename for profilers
+        # (such as cProfile) that depend on the tuple of (<filename>,
+        # <definition line>, <function name>) being unique.
+        filename = '<decorator-gen-%d>' % (next(self._compile_count),)
+        try:
+            code = compile(src, filename, 'single')
+            exec(code, evaldict)
+        except:
+            print('Error in generated code:', file=sys.stderr)
+            print(src, file=sys.stderr)
+            raise
+        func = evaldict[name]
+        if addsource:
+            attrs['__source__'] = src
+        self.update(func, **attrs)
+        return func
+
+    @classmethod
+    def create(cls, obj, body, evaldict, defaults=None,
+               doc=None, module=None, addsource=True, **attrs):
+        """
+        Create a function from the strings name, signature and body.
+        evaldict is the evaluation dictionary. If addsource is true an
+        attribute __source__ is added to the result. The attributes attrs
+        are added, if any.
+        """
+        if isinstance(obj, str):  # "name(signature)"
+            name, rest = obj.strip().split('(', 1)
+            signature = rest[:-1]  # strip a right parens
+            func = None
+        else:  # a function
+            name = None
+            signature = None
+            func = obj
+        self = cls(func, name, signature, defaults, doc, module)
+        ibody = '\n'.join('    ' + line for line in body.splitlines())
+        return self.make('def %(name)s(%(signature)s):\n' + ibody,
+                         evaldict, addsource, **attrs)
+
+
+def decorate(func, caller):
+    """
+    decorate(func, caller) decorates a function using a caller.
+    """
+    evaldict = dict(_call_=caller, _func_=func)
+    fun = FunctionMaker.create(
+        func, "return _call_(_func_, %(shortsignature)s)",
+        evaldict, __wrapped__=func)
+    if hasattr(func, '__qualname__'):
+        fun.__qualname__ = func.__qualname__
+    return fun
+
+
+def decorator(caller, _func=None):
+    """decorator(caller) converts a caller function into a decorator"""
+    if _func is not None:  # return a decorated function
+        # this is obsolete behavior; you should use decorate instead
+        return decorate(_func, caller)
+    # else return a decorator function
+    if inspect.isclass(caller):
+        name = caller.__name__.lower()
+        doc = 'decorator(%s) converts functions/generators into ' \
+              'factories of %s objects' % (caller.__name__, caller.__name__)
+    elif inspect.isfunction(caller):
+        if caller.__name__ == '<lambda>':
+            name = '_lambda_'
+        else:
+            name = caller.__name__
+        doc = caller.__doc__
+    else:  # assume caller is an object with a __call__ method
+        name = caller.__class__.__name__.lower()
+        doc = caller.__call__.__doc__
+    evaldict = dict(_call_=caller, _decorate_=decorate)
+    return FunctionMaker.create(
+        '%s(func)' % name, 'return _decorate_(func, _call_)',
+        evaldict, doc=doc, module=caller.__module__,
+        __wrapped__=caller)
+
+
+# ####################### contextmanager ####################### #
+
+try:  # Python >= 3.2
+    from contextlib import _GeneratorContextManager
+except ImportError:  # Python >= 2.5
+    from contextlib import GeneratorContextManager as _GeneratorContextManager
+
+
+class ContextManager(_GeneratorContextManager):
+    def __call__(self, func):
+        """Context manager decorator"""
+        return FunctionMaker.create(
+            func, "with _self_: return _func_(%(shortsignature)s)",
+            dict(_self_=self, _func_=func), __wrapped__=func)
+
+
+init = getfullargspec(_GeneratorContextManager.__init__)
+n_args = len(init.args)
+if n_args == 2 and not init.varargs:  # (self, genobj) Python 2.7
+    def __init__(self, g, *a, **k):
+        return _GeneratorContextManager.__init__(self, g(*a, **k))
+
+
+    ContextManager.__init__ = __init__
+elif n_args == 2 and init.varargs:  # (self, gen, *a, **k) Python 3.4
+    pass
+elif n_args == 4:  # (self, gen, args, kwds) Python 3.5
+    def __init__(self, g, *a, **k):
+        return _GeneratorContextManager.__init__(self, g, a, k)
+
+
+    ContextManager.__init__ = __init__
+
+contextmanager = decorator(ContextManager)
+
+
+# ############################ dispatch_on ############################ #
+
+def append(a, vancestors):
+    """
+    Append ``a`` to the list of the virtual ancestors, unless it is already
+    included.
+    """
+    add = True
+    for j, va in enumerate(vancestors):
+        if issubclass(va, a):
+            add = False
+            break
+        if issubclass(a, va):
+            vancestors[j] = a
+            add = False
+    if add:
+        vancestors.append(a)
+
+
+# inspired from simplegeneric by P.J. Eby and functools.singledispatch
+def dispatch_on(*dispatch_args):
+    """
+    Factory of decorators turning a function into a generic function
+    dispatching on the given arguments.
+    """
+    assert dispatch_args, 'No dispatch args passed'
+    dispatch_str = '(%s,)' % ', '.join(dispatch_args)
+
+    def check(arguments, wrong=operator.ne, msg=''):
+        """Make sure one passes the expected number of arguments"""
+        if wrong(len(arguments), len(dispatch_args)):
+            raise TypeError('Expected %d arguments, got %d%s' %
+                            (len(dispatch_args), len(arguments), msg))
+
+    def gen_func_dec(func):
+        """Decorator turning a function into a generic function"""
+
+        # first check the dispatch arguments
+        argset = set(getfullargspec(func).args)
+        if not set(dispatch_args) <= argset:
+            raise NameError('Unknown dispatch arguments %s' % dispatch_str)
+
+        typemap = {}
+
+        def vancestors(*types):
+            """
+            Get a list of sets of virtual ancestors for the given types
+            """
+            check(types)
+            ras = [[] for _ in range(len(dispatch_args))]
+            for types_ in typemap:
+                for t, type_, ra in zip(types, types_, ras):
+                    if issubclass(t, type_) and type_ not in t.__mro__:
+                        append(type_, ra)
+            return [set(ra) for ra in ras]
+
+        def ancestors(*types):
+            """
+            Get a list of virtual MROs, one for each type
+            """
+            check(types)
+            lists = []
+            for t, vas in zip(types, vancestors(*types)):
+                n_vas = len(vas)
+                if n_vas > 1:
+                    raise RuntimeError(
+                        'Ambiguous dispatch for %s: %s' % (t, vas))
+                elif n_vas == 1:
+                    va, = vas
+                    mro = type('t', (t, va), {}).__mro__[1:]
+                else:
+                    mro = t.__mro__
+                lists.append(mro[:-1])  # discard t and object
+            return lists
+
+        def register(*types):
+            """
+            Decorator to register an implementation for the given types
+            """
+            check(types)
+
+            def dec(f):
+                check(getfullargspec(f).args, operator.lt, ' in ' + f.__name__)
+                typemap[types] = f
+                return f
+
+            return dec
+
+        def dispatch_info(*types):
+            """
+            An utility to introspect the dispatch algorithm
+            """
+            check(types)
+            lst = []
+            for anc in itertools.product(*ancestors(*types)):
+                lst.append(tuple(a.__name__ for a in anc))
+            return lst
+
+        def _dispatch(dispatch_args, *args, **kw):
+            types = tuple(type(arg) for arg in dispatch_args)
+            try:  # fast path
+                f = typemap[types]
+            except KeyError:
+                pass
+            else:
+                return f(*args, **kw)
+            combinations = itertools.product(*ancestors(*types))
+            next(combinations)  # the first one has been already tried
+            for types_ in combinations:
+                f = typemap.get(types_)
+                if f is not None:
+                    return f(*args, **kw)
+
+            # else call the default implementation
+            return func(*args, **kw)
+
+        return FunctionMaker.create(
+            func, 'return _f_(%s, %%(shortsignature)s)' % dispatch_str,
+            dict(_f_=_dispatch), register=register, default=func,
+            typemap=typemap, vancestors=vancestors, ancestors=ancestors,
+            dispatch_info=dispatch_info, __wrapped__=func)
+
+    gen_func_dec.__name__ = 'dispatch_on' + dispatch_str
+    return gen_func_dec

+ 368 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/exposition.py

@@ -0,0 +1,368 @@
+#!/usr/bin/python
+
+from __future__ import unicode_literals
+
+import base64
+from contextlib import closing
+import os
+import socket
+import sys
+import threading
+from wsgiref.simple_server import make_server, WSGIRequestHandler
+
+from .openmetrics import exposition as openmetrics
+from .registry import REGISTRY
+from .utils import floatToGoString
+
+try:
+    from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
+    from SocketServer import ThreadingMixIn
+    from urllib2 import build_opener, Request, HTTPHandler
+    from urllib import quote_plus
+    from urlparse import parse_qs, urlparse
+except ImportError:
+    # Python 3
+    from http.server import BaseHTTPRequestHandler, HTTPServer
+    from socketserver import ThreadingMixIn
+    from urllib.request import build_opener, Request, HTTPHandler
+    from urllib.parse import quote_plus, parse_qs, urlparse
+
+CONTENT_TYPE_LATEST = str('text/plain; version=0.0.4; charset=utf-8')
+"""Content type of the latest text format"""
+
+PYTHON26_OR_OLDER = sys.version_info < (2, 7)
+
+
+def make_wsgi_app(registry=REGISTRY):
+    """Create a WSGI app which serves the metrics from a registry."""
+
+    def prometheus_app(environ, start_response):
+        params = parse_qs(environ.get('QUERY_STRING', ''))
+        r = registry
+        encoder, content_type = choose_encoder(environ.get('HTTP_ACCEPT'))
+        if 'name[]' in params:
+            r = r.restricted_registry(params['name[]'])
+        output = encoder(r)
+
+        status = str('200 OK')
+        headers = [(str('Content-type'), content_type)]
+        start_response(status, headers)
+        return [output]
+
+    return prometheus_app
+
+
+class _SilentHandler(WSGIRequestHandler):
+    """WSGI handler that does not log requests."""
+
+    def log_message(self, format, *args):
+        """Log nothing."""
+
+
+def start_wsgi_server(port, addr='', registry=REGISTRY):
+    """Starts a WSGI server for prometheus metrics as a daemon thread."""
+    app = make_wsgi_app(registry)
+    httpd = make_server(addr, port, app, handler_class=_SilentHandler)
+    t = threading.Thread(target=httpd.serve_forever)
+    t.daemon = True
+    t.start()
+
+
+def generate_latest(registry=REGISTRY):
+    """Returns the metrics from the registry in latest text format as a string."""
+
+    def sample_line(line):
+        if line.labels:
+            labelstr = '{{{0}}}'.format(','.join(
+                ['{0}="{1}"'.format(
+                    k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
+                    for k, v in sorted(line.labels.items())]))
+        else:
+            labelstr = ''
+        timestamp = ''
+        if line.timestamp is not None:
+            # Convert to milliseconds.
+            timestamp = ' {0:d}'.format(int(float(line.timestamp) * 1000))
+        return '{0}{1} {2}{3}\n'.format(
+            line.name, labelstr, floatToGoString(line.value), timestamp)
+
+    output = []
+    for metric in registry.collect():
+        try:
+            mname = metric.name
+            mtype = metric.type
+            # Munging from OpenMetrics into Prometheus format.
+            if mtype == 'counter':
+                mname = mname + '_total'
+            elif mtype == 'info':
+                mname = mname + '_info'
+                mtype = 'gauge'
+            elif mtype == 'stateset':
+                mtype = 'gauge'
+            elif mtype == 'gaugehistogram':
+                # A gauge histogram is really a gauge,
+                # but this captures the strucutre better.
+                mtype = 'histogram'
+            elif mtype == 'unknown':
+                mtype = 'untyped'
+
+            output.append('# HELP {0} {1}\n'.format(
+                mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
+            output.append('# TYPE {0} {1}\n'.format(mname, mtype))
+
+            om_samples = {}
+            for s in metric.samples:
+                for suffix in ['_created', '_gsum', '_gcount']:
+                    if s.name == metric.name + suffix:
+                        # OpenMetrics specific sample, put in a gauge at the end.
+                        om_samples.setdefault(suffix, []).append(sample_line(s))
+                        break
+                else:
+                    output.append(sample_line(s))
+        except Exception as exception:
+            exception.args = (exception.args or ('',)) + (metric,)
+            raise
+
+        for suffix, lines in sorted(om_samples.items()):
+            output.append('# TYPE {0}{1} gauge\n'.format(metric.name, suffix))
+            output.extend(lines)
+    return ''.join(output).encode('utf-8')
+
+
+def choose_encoder(accept_header):
+    accept_header = accept_header or ''
+    for accepted in accept_header.split(','):
+        if accepted.split(';')[0].strip() == 'application/openmetrics-text':
+            return (openmetrics.generate_latest,
+                    openmetrics.CONTENT_TYPE_LATEST)
+    return generate_latest, CONTENT_TYPE_LATEST
+
+
+class MetricsHandler(BaseHTTPRequestHandler):
+    """HTTP handler that gives metrics from ``REGISTRY``."""
+    registry = REGISTRY
+
+    def do_GET(self):
+        registry = self.registry
+        params = parse_qs(urlparse(self.path).query)
+        encoder, content_type = choose_encoder(self.headers.get('Accept'))
+        if 'name[]' in params:
+            registry = registry.restricted_registry(params['name[]'])
+        try:
+            output = encoder(registry)
+        except:
+            self.send_error(500, 'error generating metric output')
+            raise
+        self.send_response(200)
+        self.send_header('Content-Type', content_type)
+        self.end_headers()
+        self.wfile.write(output)
+
+    def log_message(self, format, *args):
+        """Log nothing."""
+
+    @classmethod
+    def factory(cls, registry):
+        """Returns a dynamic MetricsHandler class tied
+           to the passed registry.
+        """
+        # This implementation relies on MetricsHandler.registry
+        #  (defined above and defaulted to REGISTRY).
+
+        # As we have unicode_literals, we need to create a str()
+        #  object for type().
+        cls_name = str(cls.__name__)
+        MyMetricsHandler = type(cls_name, (cls, object),
+                                {"registry": registry})
+        return MyMetricsHandler
+
+
+class _ThreadingSimpleServer(ThreadingMixIn, HTTPServer):
+    """Thread per request HTTP server."""
+    # Make worker threads "fire and forget". Beginning with Python 3.7 this
+    # prevents a memory leak because ``ThreadingMixIn`` starts to gather all
+    # non-daemon threads in a list in order to join on them at server close.
+    # Enabling daemon threads virtually makes ``_ThreadingSimpleServer`` the
+    # same as Python 3.7's ``ThreadingHTTPServer``.
+    daemon_threads = True
+
+
+def start_http_server(port, addr='', registry=REGISTRY):
+    """Starts an HTTP server for prometheus metrics as a daemon thread"""
+    CustomMetricsHandler = MetricsHandler.factory(registry)
+    httpd = _ThreadingSimpleServer((addr, port), CustomMetricsHandler)
+    t = threading.Thread(target=httpd.serve_forever)
+    t.daemon = True
+    t.start()
+
+
+def write_to_textfile(path, registry):
+    """Write metrics to the given path.
+
+    This is intended for use with the Node exporter textfile collector.
+    The path must end in .prom for the textfile collector to process it."""
+    tmppath = '%s.%s.%s' % (path, os.getpid(), threading.current_thread().ident)
+    with open(tmppath, 'wb') as f:
+        f.write(generate_latest(registry))
+    # rename(2) is atomic.
+    os.rename(tmppath, path)
+
+
+def default_handler(url, method, timeout, headers, data):
+    """Default handler that implements HTTP/HTTPS connections.
+
+    Used by the push_to_gateway functions. Can be re-used by other handlers."""
+
+    def handle():
+        request = Request(url, data=data)
+        request.get_method = lambda: method
+        for k, v in headers:
+            request.add_header(k, v)
+        resp = build_opener(HTTPHandler).open(request, timeout=timeout)
+        if resp.code >= 400:
+            raise IOError("error talking to pushgateway: {0} {1}".format(
+                resp.code, resp.msg))
+
+    return handle
+
+
+def basic_auth_handler(url, method, timeout, headers, data, username=None, password=None):
+    """Handler that implements HTTP/HTTPS connections with Basic Auth.
+
+    Sets auth headers using supplied 'username' and 'password', if set.
+    Used by the push_to_gateway functions. Can be re-used by other handlers."""
+
+    def handle():
+        """Handler that implements HTTP Basic Auth.
+        """
+        if username is not None and password is not None:
+            auth_value = '{0}:{1}'.format(username, password).encode('utf-8')
+            auth_token = base64.b64encode(auth_value)
+            auth_header = b'Basic ' + auth_token
+            headers.append(['Authorization', auth_header])
+        default_handler(url, method, timeout, headers, data)()
+
+    return handle
+
+
+def push_to_gateway(
+        gateway, job, registry, grouping_key=None, timeout=30,
+        handler=default_handler):
+    """Push metrics to the given pushgateway.
+
+    `gateway` the url for your push gateway. Either of the form
+              'http://pushgateway.local', or 'pushgateway.local'.
+              Scheme defaults to 'http' if none is provided
+    `job` is the job label to be attached to all pushed metrics
+    `registry` is an instance of CollectorRegistry
+    `grouping_key` please see the pushgateway documentation for details.
+                   Defaults to None
+    `timeout` is how long push will attempt to connect before giving up.
+              Defaults to 30s, can be set to None for no timeout.
+    `handler` is an optional function which can be provided to perform
+              requests to the 'gateway'.
+              Defaults to None, in which case an http or https request
+              will be carried out by a default handler.
+              If not None, the argument must be a function which accepts
+              the following arguments:
+              url, method, timeout, headers, and content
+              May be used to implement additional functionality not
+              supported by the built-in default handler (such as SSL
+              client certicates, and HTTP authentication mechanisms).
+              'url' is the URL for the request, the 'gateway' argument
+              described earlier will form the basis of this URL.
+              'method' is the HTTP method which should be used when
+              carrying out the request.
+              'timeout' requests not successfully completed after this
+              many seconds should be aborted.  If timeout is None, then
+              the handler should not set a timeout.
+              'headers' is a list of ("header-name","header-value") tuples
+              which must be passed to the pushgateway in the form of HTTP
+              request headers.
+              The function should raise an exception (e.g. IOError) on
+              failure.
+              'content' is the data which should be used to form the HTTP
+              Message Body.
+
+    This overwrites all metrics with the same job and grouping_key.
+    This uses the PUT HTTP method."""
+    _use_gateway('PUT', gateway, job, registry, grouping_key, timeout, handler)
+
+
+def pushadd_to_gateway(
+        gateway, job, registry, grouping_key=None, timeout=30,
+        handler=default_handler):
+    """PushAdd metrics to the given pushgateway.
+
+    `gateway` the url for your push gateway. Either of the form
+              'http://pushgateway.local', or 'pushgateway.local'.
+              Scheme defaults to 'http' if none is provided
+    `job` is the job label to be attached to all pushed metrics
+    `registry` is an instance of CollectorRegistry
+    `grouping_key` please see the pushgateway documentation for details.
+                   Defaults to None
+    `timeout` is how long push will attempt to connect before giving up.
+              Defaults to 30s, can be set to None for no timeout.
+    `handler` is an optional function which can be provided to perform
+              requests to the 'gateway'.
+              Defaults to None, in which case an http or https request
+              will be carried out by a default handler.
+              See the 'prometheus_client.push_to_gateway' documentation
+              for implementation requirements.
+
+    This replaces metrics with the same name, job and grouping_key.
+    This uses the POST HTTP method."""
+    _use_gateway('POST', gateway, job, registry, grouping_key, timeout, handler)
+
+
+def delete_from_gateway(
+        gateway, job, grouping_key=None, timeout=30, handler=default_handler):
+    """Delete metrics from the given pushgateway.
+
+    `gateway` the url for your push gateway. Either of the form
+              'http://pushgateway.local', or 'pushgateway.local'.
+              Scheme defaults to 'http' if none is provided
+    `job` is the job label to be attached to all pushed metrics
+    `grouping_key` please see the pushgateway documentation for details.
+                   Defaults to None
+    `timeout` is how long delete will attempt to connect before giving up.
+              Defaults to 30s, can be set to None for no timeout.
+    `handler` is an optional function which can be provided to perform
+              requests to the 'gateway'.
+              Defaults to None, in which case an http or https request
+              will be carried out by a default handler.
+              See the 'prometheus_client.push_to_gateway' documentation
+              for implementation requirements.
+
+    This deletes metrics with the given job and grouping_key.
+    This uses the DELETE HTTP method."""
+    _use_gateway('DELETE', gateway, job, None, grouping_key, timeout, handler)
+
+
+def _use_gateway(method, gateway, job, registry, grouping_key, timeout, handler):
+    gateway_url = urlparse(gateway)
+    if not gateway_url.scheme or (PYTHON26_OR_OLDER and gateway_url.scheme not in ['http', 'https']):
+        gateway = 'http://{0}'.format(gateway)
+    url = '{0}/metrics/job/{1}'.format(gateway, quote_plus(job))
+
+    data = b''
+    if method != 'DELETE':
+        data = generate_latest(registry)
+
+    if grouping_key is None:
+        grouping_key = {}
+    url += ''.join(
+        '/{0}/{1}'.format(quote_plus(str(k)), quote_plus(str(v)))
+        for k, v in sorted(grouping_key.items()))
+
+    handler(
+        url=url, method=method, timeout=timeout,
+        headers=[('Content-Type', CONTENT_TYPE_LATEST)], data=data,
+    )()
+
+
+def instance_ip_grouping_key():
+    """Grouping key with instance set to the IP Address of this host."""
+    with closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as s:
+        s.connect(('localhost', 0))
+        return {'instance': s.getsockname()[0]}

+ 48 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/gc_collector.py

@@ -0,0 +1,48 @@
+#!/usr/bin/python
+
+from __future__ import unicode_literals
+
+import gc
+import platform
+
+from .metrics_core import CounterMetricFamily
+from .registry import REGISTRY
+
+
+class GCCollector(object):
+    """Collector for Garbage collection statistics."""
+
+    def __init__(self, registry=REGISTRY):
+        if not hasattr(gc, 'get_stats') or platform.python_implementation() != 'CPython':
+            return
+        registry.register(self)
+
+    def collect(self):
+        collected = CounterMetricFamily(
+            'python_gc_objects_collected',
+            'Objects collected during gc',
+            labels=['generation'],
+        )
+        uncollectable = CounterMetricFamily(
+            'python_gc_objects_uncollectable',
+            'Uncollectable object found during GC',
+            labels=['generation'],
+        )
+
+        collections = CounterMetricFamily(
+            'python_gc_collections',
+            'Number of times this generation was collected',
+            labels=['generation'],
+        )
+
+        for generation, stat in enumerate(gc.get_stats()):
+            generation = str(generation)
+            collected.add_metric([generation], value=stat['collected'])
+            uncollectable.add_metric([generation], value=stat['uncollectable'])
+            collections.add_metric([generation], value=stat['collections'])
+
+        return [collected, uncollectable, collections]
+
+
+GC_COLLECTOR = GCCollector()
+"""Default GCCollector in default Registry REGISTRY."""

+ 649 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/metrics.py

@@ -0,0 +1,649 @@
+import sys
+from threading import Lock
+import time
+import types
+
+from . import values  # retain this import style for testability
+from .context_managers import ExceptionCounter, InprogressTracker, Timer
+from .metrics_core import (
+    Metric, METRIC_LABEL_NAME_RE, METRIC_NAME_RE,
+    RESERVED_METRIC_LABEL_NAME_RE,
+)
+from .registry import REGISTRY
+from .utils import floatToGoString, INF
+
+if sys.version_info > (3,):
+    unicode = str
+    create_bound_method = types.MethodType
+else:
+    def create_bound_method(func, obj):
+        return types.MethodType(func, obj, obj.__class__)
+
+
+def _build_full_name(metric_type, name, namespace, subsystem, unit):
+    full_name = ''
+    if namespace:
+        full_name += namespace + '_'
+    if subsystem:
+        full_name += subsystem + '_'
+    full_name += name
+    if unit and not full_name.endswith("_" + unit):
+        full_name += "_" + unit
+    if unit and metric_type in ('info', 'stateset'):
+        raise ValueError('Metric name is of a type that cannot have a unit: ' + full_name)
+    if metric_type == 'counter' and full_name.endswith('_total'):
+        full_name = full_name[:-6]  # Munge to OpenMetrics.
+    return full_name
+
+
+def _validate_labelnames(cls, labelnames):
+    labelnames = tuple(labelnames)
+    for l in labelnames:
+        if not METRIC_LABEL_NAME_RE.match(l):
+            raise ValueError('Invalid label metric name: ' + l)
+        if RESERVED_METRIC_LABEL_NAME_RE.match(l):
+            raise ValueError('Reserved label metric name: ' + l)
+        if l in cls._reserved_labelnames:
+            raise ValueError('Reserved label metric name: ' + l)
+    return labelnames
+
+
+class MetricWrapperBase(object):
+    _type = None
+    _reserved_labelnames = ()
+
+    def _is_observable(self):
+        # Whether this metric is observable, i.e.
+        # * a metric without label names and values, or
+        # * the child of a labelled metric.
+        return not self._labelnames or (self._labelnames and self._labelvalues)
+
+    def _is_parent(self):
+        return self._labelnames and not self._labelvalues
+
+    def _get_metric(self):
+        return Metric(self._name, self._documentation, self._type, self._unit)
+
+    def describe(self):
+        return [self._get_metric()]
+
+    def collect(self):
+        metric = self._get_metric()
+        for suffix, labels, value in self._samples():
+            metric.add_sample(self._name + suffix, labels, value)
+        return [metric]
+
+    def __init__(self,
+                 name,
+                 documentation,
+                 labelnames=(),
+                 namespace='',
+                 subsystem='',
+                 unit='',
+                 registry=REGISTRY,
+                 labelvalues=None,
+                 ):
+        self._name = _build_full_name(self._type, name, namespace, subsystem, unit)
+        self._labelnames = _validate_labelnames(self, labelnames)
+        self._labelvalues = tuple(labelvalues or ())
+        self._kwargs = {}
+        self._documentation = documentation
+        self._unit = unit
+
+        if not METRIC_NAME_RE.match(self._name):
+            raise ValueError('Invalid metric name: ' + self._name)
+
+        if self._is_parent():
+            # Prepare the fields needed for child metrics.
+            self._lock = Lock()
+            self._metrics = {}
+
+        if self._is_observable():
+            self._metric_init()
+
+        if not self._labelvalues:
+            # Register the multi-wrapper parent metric, or if a label-less metric, the whole shebang.
+            if registry:
+                registry.register(self)
+
+    def labels(self, *labelvalues, **labelkwargs):
+        """Return the child for the given labelset.
+
+        All metrics can have labels, allowing grouping of related time series.
+        Taking a counter as an example:
+
+            from prometheus_client import Counter
+
+            c = Counter('my_requests_total', 'HTTP Failures', ['method', 'endpoint'])
+            c.labels('get', '/').inc()
+            c.labels('post', '/submit').inc()
+
+        Labels can also be provided as keyword arguments:
+
+            from prometheus_client import Counter
+
+            c = Counter('my_requests_total', 'HTTP Failures', ['method', 'endpoint'])
+            c.labels(method='get', endpoint='/').inc()
+            c.labels(method='post', endpoint='/submit').inc()
+
+        See the best practices on [naming](http://prometheus.io/docs/practices/naming/)
+        and [labels](http://prometheus.io/docs/practices/instrumentation/#use-labels).
+        """
+        if not self._labelnames:
+            raise ValueError('No label names were set when constructing %s' % self)
+
+        if self._labelvalues:
+            raise ValueError('%s already has labels set (%s); can not chain calls to .labels()' % (
+                self,
+                dict(zip(self._labelnames, self._labelvalues))
+            ))
+
+        if labelvalues and labelkwargs:
+            raise ValueError("Can't pass both *args and **kwargs")
+
+        if labelkwargs:
+            if sorted(labelkwargs) != sorted(self._labelnames):
+                raise ValueError('Incorrect label names')
+            labelvalues = tuple(unicode(labelkwargs[l]) for l in self._labelnames)
+        else:
+            if len(labelvalues) != len(self._labelnames):
+                raise ValueError('Incorrect label count')
+            labelvalues = tuple(unicode(l) for l in labelvalues)
+        with self._lock:
+            if labelvalues not in self._metrics:
+                self._metrics[labelvalues] = self.__class__(
+                    self._name,
+                    documentation=self._documentation,
+                    labelnames=self._labelnames,
+                    unit=self._unit,
+                    labelvalues=labelvalues,
+                    **self._kwargs
+                )
+            return self._metrics[labelvalues]
+
+    def remove(self, *labelvalues):
+        if not self._labelnames:
+            raise ValueError('No label names were set when constructing %s' % self)
+
+        """Remove the given labelset from the metric."""
+        if len(labelvalues) != len(self._labelnames):
+            raise ValueError('Incorrect label count (expected %d, got %s)' % (len(self._labelnames), labelvalues))
+        labelvalues = tuple(unicode(l) for l in labelvalues)
+        with self._lock:
+            del self._metrics[labelvalues]
+
+    def _samples(self):
+        if self._is_parent():
+            return self._multi_samples()
+        else:
+            return self._child_samples()
+
+    def _multi_samples(self):
+        with self._lock:
+            metrics = self._metrics.copy()
+        for labels, metric in metrics.items():
+            series_labels = list(zip(self._labelnames, labels))
+            for suffix, sample_labels, value in metric._samples():
+                yield (suffix, dict(series_labels + list(sample_labels.items())), value)
+
+    def _child_samples(self):  # pragma: no cover
+        raise NotImplementedError('_child_samples() must be implemented by %r' % self)
+
+    def _metric_init(self):  # pragma: no cover
+        """
+        Initialize the metric object as a child, i.e. when it has labels (if any) set.
+
+        This is factored as a separate function to allow for deferred initialization.
+        """
+        raise NotImplementedError('_metric_init() must be implemented by %r' % self)
+
+
+class Counter(MetricWrapperBase):
+    """A Counter tracks counts of events or running totals.
+
+    Example use cases for Counters:
+    - Number of requests processed
+    - Number of items that were inserted into a queue
+    - Total amount of data that a system has processed
+
+    Counters can only go up (and be reset when the process restarts). If your use case can go down,
+    you should use a Gauge instead.
+
+    An example for a Counter:
+
+        from prometheus_client import Counter
+
+        c = Counter('my_failures_total', 'Description of counter')
+        c.inc()     # Increment by 1
+        c.inc(1.6)  # Increment by given value
+
+    There are utilities to count exceptions raised:
+
+        @c.count_exceptions()
+        def f():
+            pass
+
+        with c.count_exceptions():
+            pass
+
+        # Count only one type of exception
+        with c.count_exceptions(ValueError):
+            pass
+    """
+    _type = 'counter'
+
+    def _metric_init(self):
+        self._value = values.ValueClass(self._type, self._name, self._name + '_total', self._labelnames,
+                                        self._labelvalues)
+        self._created = time.time()
+
+    def inc(self, amount=1):
+        """Increment counter by the given amount."""
+        if amount < 0:
+            raise ValueError('Counters can only be incremented by non-negative amounts.')
+        self._value.inc(amount)
+
+    def count_exceptions(self, exception=Exception):
+        """Count exceptions in a block of code or function.
+
+        Can be used as a function decorator or context manager.
+        Increments the counter when an exception of the given
+        type is raised up out of the code.
+        """
+        return ExceptionCounter(self, exception)
+
+    def _child_samples(self):
+        return (
+            ('_total', {}, self._value.get()),
+            ('_created', {}, self._created),
+        )
+
+
+class Gauge(MetricWrapperBase):
+    """Gauge metric, to report instantaneous values.
+
+     Examples of Gauges include:
+        - Inprogress requests
+        - Number of items in a queue
+        - Free memory
+        - Total memory
+        - Temperature
+
+     Gauges can go both up and down.
+
+        from prometheus_client import Gauge
+
+        g = Gauge('my_inprogress_requests', 'Description of gauge')
+        g.inc()      # Increment by 1
+        g.dec(10)    # Decrement by given value
+        g.set(4.2)   # Set to a given value
+
+     There are utilities for common use cases:
+
+        g.set_to_current_time()   # Set to current unixtime
+
+        # Increment when entered, decrement when exited.
+        @g.track_inprogress()
+        def f():
+            pass
+
+        with g.track_inprogress():
+            pass
+
+     A Gauge can also take its value from a callback:
+
+        d = Gauge('data_objects', 'Number of objects')
+        my_dict = {}
+        d.set_function(lambda: len(my_dict))
+    """
+    _type = 'gauge'
+    _MULTIPROC_MODES = frozenset(('min', 'max', 'livesum', 'liveall', 'all'))
+
+    def __init__(self,
+                 name,
+                 documentation,
+                 labelnames=(),
+                 namespace='',
+                 subsystem='',
+                 unit='',
+                 registry=REGISTRY,
+                 labelvalues=None,
+                 multiprocess_mode='all',
+                 ):
+        self._multiprocess_mode = multiprocess_mode
+        if multiprocess_mode not in self._MULTIPROC_MODES:
+            raise ValueError('Invalid multiprocess mode: ' + multiprocess_mode)
+        super(Gauge, self).__init__(
+            name=name,
+            documentation=documentation,
+            labelnames=labelnames,
+            namespace=namespace,
+            subsystem=subsystem,
+            unit=unit,
+            registry=registry,
+            labelvalues=labelvalues,
+        )
+        self._kwargs['multiprocess_mode'] = self._multiprocess_mode
+
+    def _metric_init(self):
+        self._value = values.ValueClass(
+            self._type, self._name, self._name, self._labelnames, self._labelvalues,
+            multiprocess_mode=self._multiprocess_mode
+        )
+
+    def inc(self, amount=1):
+        """Increment gauge by the given amount."""
+        self._value.inc(amount)
+
+    def dec(self, amount=1):
+        """Decrement gauge by the given amount."""
+        self._value.inc(-amount)
+
+    def set(self, value):
+        """Set gauge to the given value."""
+        self._value.set(float(value))
+
+    def set_to_current_time(self):
+        """Set gauge to the current unixtime."""
+        self.set(time.time())
+
+    def track_inprogress(self):
+        """Track inprogress blocks of code or functions.
+
+        Can be used as a function decorator or context manager.
+        Increments the gauge when the code is entered,
+        and decrements when it is exited.
+        """
+        return InprogressTracker(self)
+
+    def time(self):
+        """Time a block of code or function, and set the duration in seconds.
+
+        Can be used as a function decorator or context manager.
+        """
+        return Timer(self.set)
+
+    def set_function(self, f):
+        """Call the provided function to return the Gauge value.
+
+        The function must return a float, and may be called from
+        multiple threads. All other methods of the Gauge become NOOPs.
+        """
+
+        def samples(self):
+            return (('', {}, float(f())),)
+
+        self._child_samples = create_bound_method(samples, self)
+
+    def _child_samples(self):
+        return (('', {}, self._value.get()),)
+
+
+class Summary(MetricWrapperBase):
+    """A Summary tracks the size and number of events.
+
+    Example use cases for Summaries:
+    - Response latency
+    - Request size
+
+    Example for a Summary:
+
+        from prometheus_client import Summary
+
+        s = Summary('request_size_bytes', 'Request size (bytes)')
+        s.observe(512)  # Observe 512 (bytes)
+
+    Example for a Summary using time:
+
+        from prometheus_client import Summary
+
+        REQUEST_TIME = Summary('response_latency_seconds', 'Response latency (seconds)')
+
+        @REQUEST_TIME.time()
+        def create_response(request):
+          '''A dummy function'''
+          time.sleep(1)
+
+    Example for using the same Summary object as a context manager:
+
+        with REQUEST_TIME.time():
+            pass  # Logic to be timed
+    """
+    _type = 'summary'
+    _reserved_labelnames = ['quantile']
+
+    def _metric_init(self):
+        self._count = values.ValueClass(self._type, self._name, self._name + '_count', self._labelnames,
+                                        self._labelvalues)
+        self._sum = values.ValueClass(self._type, self._name, self._name + '_sum', self._labelnames, self._labelvalues)
+        self._created = time.time()
+
+    def observe(self, amount):
+        """Observe the given amount."""
+        self._count.inc(1)
+        self._sum.inc(amount)
+
+    def time(self):
+        """Time a block of code or function, and observe the duration in seconds.
+
+        Can be used as a function decorator or context manager.
+        """
+        return Timer(self.observe)
+
+    def _child_samples(self):
+        return (
+            ('_count', {}, self._count.get()),
+            ('_sum', {}, self._sum.get()),
+            ('_created', {}, self._created))
+
+
+class Histogram(MetricWrapperBase):
+    """A Histogram tracks the size and number of events in buckets.
+
+    You can use Histograms for aggregatable calculation of quantiles.
+
+    Example use cases:
+    - Response latency
+    - Request size
+
+    Example for a Histogram:
+
+        from prometheus_client import Histogram
+
+        h = Histogram('request_size_bytes', 'Request size (bytes)')
+        h.observe(512)  # Observe 512 (bytes)
+
+    Example for a Histogram using time:
+
+        from prometheus_client import Histogram
+
+        REQUEST_TIME = Histogram('response_latency_seconds', 'Response latency (seconds)')
+
+        @REQUEST_TIME.time()
+        def create_response(request):
+          '''A dummy function'''
+          time.sleep(1)
+
+    Example of using the same Histogram object as a context manager:
+
+        with REQUEST_TIME.time():
+            pass  # Logic to be timed
+
+    The default buckets are intended to cover a typical web/rpc request from milliseconds to seconds.
+    They can be overridden by passing `buckets` keyword argument to `Histogram`.
+    """
+    _type = 'histogram'
+    _reserved_labelnames = ['le']
+    DEFAULT_BUCKETS = (.005, .01, .025, .05, .075, .1, .25, .5, .75, 1.0, 2.5, 5.0, 7.5, 10.0, INF)
+
+    def __init__(self,
+                 name,
+                 documentation,
+                 labelnames=(),
+                 namespace='',
+                 subsystem='',
+                 unit='',
+                 registry=REGISTRY,
+                 labelvalues=None,
+                 buckets=DEFAULT_BUCKETS,
+                 ):
+        self._prepare_buckets(buckets)
+        super(Histogram, self).__init__(
+            name=name,
+            documentation=documentation,
+            labelnames=labelnames,
+            namespace=namespace,
+            subsystem=subsystem,
+            unit=unit,
+            registry=registry,
+            labelvalues=labelvalues,
+        )
+        self._kwargs['buckets'] = buckets
+
+    def _prepare_buckets(self, buckets):
+        buckets = [float(b) for b in buckets]
+        if buckets != sorted(buckets):
+            # This is probably an error on the part of the user,
+            # so raise rather than sorting for them.
+            raise ValueError('Buckets not in sorted order')
+        if buckets and buckets[-1] != INF:
+            buckets.append(INF)
+        if len(buckets) < 2:
+            raise ValueError('Must have at least two buckets')
+        self._upper_bounds = buckets
+
+    def _metric_init(self):
+        self._buckets = []
+        self._created = time.time()
+        bucket_labelnames = self._labelnames + ('le',)
+        self._sum = values.ValueClass(self._type, self._name, self._name + '_sum', self._labelnames, self._labelvalues)
+        for b in self._upper_bounds:
+            self._buckets.append(values.ValueClass(
+                self._type,
+                self._name,
+                self._name + '_bucket',
+                bucket_labelnames,
+                self._labelvalues + (floatToGoString(b),))
+            )
+
+    def observe(self, amount):
+        """Observe the given amount."""
+        self._sum.inc(amount)
+        for i, bound in enumerate(self._upper_bounds):
+            if amount <= bound:
+                self._buckets[i].inc(1)
+                break
+
+    def time(self):
+        """Time a block of code or function, and observe the duration in seconds.
+
+        Can be used as a function decorator or context manager.
+        """
+        return Timer(self.observe)
+
+    def _child_samples(self):
+        samples = []
+        acc = 0
+        for i, bound in enumerate(self._upper_bounds):
+            acc += self._buckets[i].get()
+            samples.append(('_bucket', {'le': floatToGoString(bound)}, acc))
+        samples.append(('_count', {}, acc))
+        samples.append(('_sum', {}, self._sum.get()))
+        samples.append(('_created', {}, self._created))
+        return tuple(samples)
+
+
+class Info(MetricWrapperBase):
+    """Info metric, key-value pairs.
+
+     Examples of Info include:
+        - Build information
+        - Version information
+        - Potential target metadata
+
+     Example usage:
+        from prometheus_client import Info
+
+        i = Info('my_build', 'Description of info')
+        i.info({'version': '1.2.3', 'buildhost': 'foo@bar'})
+
+     Info metrics do not work in multiprocess mode.
+    """
+    _type = 'info'
+
+    def _metric_init(self):
+        self._labelname_set = set(self._labelnames)
+        self._lock = Lock()
+        self._value = {}
+
+    def info(self, val):
+        """Set info metric."""
+        if self._labelname_set.intersection(val.keys()):
+            raise ValueError('Overlapping labels for Info metric, metric: %s child: %s' % (
+                self._labelnames, val))
+        with self._lock:
+            self._value = dict(val)
+
+    def _child_samples(self):
+        with self._lock:
+            return (('_info', self._value, 1.0,),)
+
+
+class Enum(MetricWrapperBase):
+    """Enum metric, which of a set of states is true.
+
+     Example usage:
+        from prometheus_client import Enum
+
+        e = Enum('task_state', 'Description of enum',
+          states=['starting', 'running', 'stopped'])
+        e.state('running')
+
+     The first listed state will be the default.
+     Enum metrics do not work in multiprocess mode.
+    """
+    _type = 'stateset'
+
+    def __init__(self,
+                 name,
+                 documentation,
+                 labelnames=(),
+                 namespace='',
+                 subsystem='',
+                 unit='',
+                 registry=REGISTRY,
+                 labelvalues=None,
+                 states=None,
+                 ):
+        super(Enum, self).__init__(
+            name=name,
+            documentation=documentation,
+            labelnames=labelnames,
+            namespace=namespace,
+            subsystem=subsystem,
+            unit=unit,
+            registry=registry,
+            labelvalues=labelvalues,
+        )
+        if name in labelnames:
+            raise ValueError('Overlapping labels for Enum metric: %s' % (name,))
+        if not states:
+            raise ValueError('No states provided for Enum metric: %s' % (name,))
+        self._kwargs['states'] = self._states = states
+
+    def _metric_init(self):
+        self._value = 0
+        self._lock = Lock()
+
+    def state(self, state):
+        """Set enum metric state."""
+        with self._lock:
+            self._value = self._states.index(state)
+
+    def _child_samples(self):
+        with self._lock:
+            return [
+                ('', {self._name: s}, 1 if i == self._value else 0,)
+                for i, s
+                in enumerate(self._states)
+            ]

+ 325 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/metrics_core.py

@@ -0,0 +1,325 @@
+import re
+
+from .samples import Sample
+
+METRIC_TYPES = (
+    'counter', 'gauge', 'summary', 'histogram',
+    'gaugehistogram', 'unknown', 'info', 'stateset',
+)
+METRIC_NAME_RE = re.compile(r'^[a-zA-Z_:][a-zA-Z0-9_:]*$')
+METRIC_LABEL_NAME_RE = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*$')
+RESERVED_METRIC_LABEL_NAME_RE = re.compile(r'^__.*$')
+
+
+class Metric(object):
+    """A single metric family and its samples.
+
+    This is intended only for internal use by the instrumentation client.
+
+    Custom collectors should use GaugeMetricFamily, CounterMetricFamily
+    and SummaryMetricFamily instead.
+    """
+
+    def __init__(self, name, documentation, typ, unit=''):
+        if unit and not name.endswith("_" + unit):
+            name += "_" + unit
+        if not METRIC_NAME_RE.match(name):
+            raise ValueError('Invalid metric name: ' + name)
+        self.name = name
+        self.documentation = documentation
+        self.unit = unit
+        if typ == 'untyped':
+            typ = 'unknown'
+        if typ not in METRIC_TYPES:
+            raise ValueError('Invalid metric type: ' + typ)
+        self.type = typ
+        self.samples = []
+
+    def add_sample(self, name, labels, value, timestamp=None, exemplar=None):
+        """Add a sample to the metric.
+
+        Internal-only, do not use."""
+        self.samples.append(Sample(name, labels, value, timestamp, exemplar))
+
+    def __eq__(self, other):
+        return (isinstance(other, Metric) and
+                self.name == other.name and
+                self.documentation == other.documentation and
+                self.type == other.type and
+                self.unit == other.unit and
+                self.samples == other.samples)
+
+    def __repr__(self):
+        return "Metric(%s, %s, %s, %s, %s)" % (
+            self.name,
+            self.documentation,
+            self.type,
+            self.unit,
+            self.samples,
+        )
+
+
+class UnknownMetricFamily(Metric):
+    """A single unknwon metric and its samples.
+    For use by custom collectors.
+    """
+
+    def __init__(self, name, documentation, value=None, labels=None, unit=''):
+        Metric.__init__(self, name, documentation, 'unknown', unit)
+        if labels is not None and value is not None:
+            raise ValueError('Can only specify at most one of value and labels.')
+        if labels is None:
+            labels = []
+        self._labelnames = tuple(labels)
+        if value is not None:
+            self.add_metric([], value)
+
+    def add_metric(self, labels, value, timestamp=None):
+        """Add a metric to the metric family.
+        Args:
+        labels: A list of label values
+        value: The value of the metric.
+        """
+        self.samples.append(Sample(self.name, dict(zip(self._labelnames, labels)), value, timestamp))
+
+
+# For backward compatibility.
+UntypedMetricFamily = UnknownMetricFamily
+
+
+class CounterMetricFamily(Metric):
+    """A single counter and its samples.
+
+    For use by custom collectors.
+    """
+
+    def __init__(self, name, documentation, value=None, labels=None, created=None, unit=''):
+        # Glue code for pre-OpenMetrics metrics.
+        if name.endswith('_total'):
+            name = name[:-6]
+        Metric.__init__(self, name, documentation, 'counter', unit)
+        if labels is not None and value is not None:
+            raise ValueError('Can only specify at most one of value and labels.')
+        if labels is None:
+            labels = []
+        self._labelnames = tuple(labels)
+        if value is not None:
+            self.add_metric([], value, created)
+
+    def add_metric(self, labels, value, created=None, timestamp=None):
+        """Add a metric to the metric family.
+
+        Args:
+          labels: A list of label values
+          value: The value of the metric
+          created: Optional unix timestamp the child was created at.
+        """
+        self.samples.append(Sample(self.name + '_total', dict(zip(self._labelnames, labels)), value, timestamp))
+        if created is not None:
+            self.samples.append(Sample(self.name + '_created', dict(zip(self._labelnames, labels)), created, timestamp))
+
+
+class GaugeMetricFamily(Metric):
+    """A single gauge and its samples.
+
+    For use by custom collectors.
+    """
+
+    def __init__(self, name, documentation, value=None, labels=None, unit=''):
+        Metric.__init__(self, name, documentation, 'gauge', unit)
+        if labels is not None and value is not None:
+            raise ValueError('Can only specify at most one of value and labels.')
+        if labels is None:
+            labels = []
+        self._labelnames = tuple(labels)
+        if value is not None:
+            self.add_metric([], value)
+
+    def add_metric(self, labels, value, timestamp=None):
+        """Add a metric to the metric family.
+
+        Args:
+          labels: A list of label values
+          value: A float
+        """
+        self.samples.append(Sample(self.name, dict(zip(self._labelnames, labels)), value, timestamp))
+
+
+class SummaryMetricFamily(Metric):
+    """A single summary and its samples.
+
+    For use by custom collectors.
+    """
+
+    def __init__(self, name, documentation, count_value=None, sum_value=None, labels=None, unit=''):
+        Metric.__init__(self, name, documentation, 'summary', unit)
+        if (sum_value is None) != (count_value is None):
+            raise ValueError('count_value and sum_value must be provided together.')
+        if labels is not None and count_value is not None:
+            raise ValueError('Can only specify at most one of value and labels.')
+        if labels is None:
+            labels = []
+        self._labelnames = tuple(labels)
+        if count_value is not None:
+            self.add_metric([], count_value, sum_value)
+
+    def add_metric(self, labels, count_value, sum_value, timestamp=None):
+        """Add a metric to the metric family.
+
+        Args:
+          labels: A list of label values
+          count_value: The count value of the metric.
+          sum_value: The sum value of the metric.
+        """
+        self.samples.append(Sample(self.name + '_count', dict(zip(self._labelnames, labels)), count_value, timestamp))
+        self.samples.append(Sample(self.name + '_sum', dict(zip(self._labelnames, labels)), sum_value, timestamp))
+
+
+class HistogramMetricFamily(Metric):
+    """A single histogram and its samples.
+
+    For use by custom collectors.
+    """
+
+    def __init__(self, name, documentation, buckets=None, sum_value=None, labels=None, unit=''):
+        Metric.__init__(self, name, documentation, 'histogram', unit)
+        if (sum_value is None) != (buckets is None):
+            raise ValueError('buckets and sum_value must be provided together.')
+        if labels is not None and buckets is not None:
+            raise ValueError('Can only specify at most one of buckets and labels.')
+        if labels is None:
+            labels = []
+        self._labelnames = tuple(labels)
+        if buckets is not None:
+            self.add_metric([], buckets, sum_value)
+
+    def add_metric(self, labels, buckets, sum_value, timestamp=None):
+        """Add a metric to the metric family.
+
+        Args:
+          labels: A list of label values
+          buckets: A list of lists.
+              Each inner list can be a pair of bucket name and value,
+              or a triple of bucket name, value, and exemplar.
+              The buckets must be sorted, and +Inf present.
+          sum_value: The sum value of the metric.
+        """
+        for b in buckets:
+            bucket, value = b[:2]
+            exemplar = None
+            if len(b) == 3:
+                exemplar = b[2]
+            self.samples.append(Sample(
+                self.name + '_bucket',
+                dict(list(zip(self._labelnames, labels)) + [('le', bucket)]),
+                value,
+                timestamp,
+                exemplar,
+            ))
+        # +Inf is last and provides the count value.
+        self.samples.extend([
+            Sample(self.name + '_count', dict(zip(self._labelnames, labels)), buckets[-1][1], timestamp),
+            Sample(self.name + '_sum', dict(zip(self._labelnames, labels)), sum_value, timestamp),
+        ])
+
+
+class GaugeHistogramMetricFamily(Metric):
+    """A single gauge histogram and its samples.
+
+    For use by custom collectors.
+    """
+
+    def __init__(self, name, documentation, buckets=None, gsum_value=None, labels=None, unit=''):
+        Metric.__init__(self, name, documentation, 'gaugehistogram', unit)
+        if labels is not None and buckets is not None:
+            raise ValueError('Can only specify at most one of buckets and labels.')
+        if labels is None:
+            labels = []
+        self._labelnames = tuple(labels)
+        if buckets is not None:
+            self.add_metric([], buckets, gsum_value)
+
+    def add_metric(self, labels, buckets, gsum_value, timestamp=None):
+        """Add a metric to the metric family.
+
+        Args:
+          labels: A list of label values
+          buckets: A list of pairs of bucket names and values.
+              The buckets must be sorted, and +Inf present.
+          gsum_value: The sum value of the metric.
+        """
+        for bucket, value in buckets:
+            self.samples.append(Sample(
+                self.name + '_bucket',
+                dict(list(zip(self._labelnames, labels)) + [('le', bucket)]),
+                value, timestamp))
+        # +Inf is last and provides the count value.
+        self.samples.extend([
+            Sample(self.name + '_gcount', dict(zip(self._labelnames, labels)), buckets[-1][1], timestamp),
+            Sample(self.name + '_gsum', dict(zip(self._labelnames, labels)), gsum_value, timestamp),
+        ])
+
+
+class InfoMetricFamily(Metric):
+    """A single info and its samples.
+
+    For use by custom collectors.
+    """
+
+    def __init__(self, name, documentation, value=None, labels=None):
+        Metric.__init__(self, name, documentation, 'info')
+        if labels is not None and value is not None:
+            raise ValueError('Can only specify at most one of value and labels.')
+        if labels is None:
+            labels = []
+        self._labelnames = tuple(labels)
+        if value is not None:
+            self.add_metric([], value)
+
+    def add_metric(self, labels, value, timestamp=None):
+        """Add a metric to the metric family.
+
+        Args:
+          labels: A list of label values
+          value: A dict of labels
+        """
+        self.samples.append(Sample(
+            self.name + '_info',
+            dict(dict(zip(self._labelnames, labels)), **value),
+            1,
+            timestamp,
+        ))
+
+
+class StateSetMetricFamily(Metric):
+    """A single stateset and its samples.
+
+    For use by custom collectors.
+    """
+
+    def __init__(self, name, documentation, value=None, labels=None):
+        Metric.__init__(self, name, documentation, 'stateset')
+        if labels is not None and value is not None:
+            raise ValueError('Can only specify at most one of value and labels.')
+        if labels is None:
+            labels = []
+        self._labelnames = tuple(labels)
+        if value is not None:
+            self.add_metric([], value)
+
+    def add_metric(self, labels, value, timestamp=None):
+        """Add a metric to the metric family.
+
+        Args:
+          labels: A list of label values
+          value: A dict of string state names to booleans
+        """
+        labels = tuple(labels)
+        for state, enabled in sorted(value.items()):
+            v = (1 if enabled else 0)
+            self.samples.append(Sample(
+                self.name,
+                dict(zip(self._labelnames + (self.name,), labels + (state,))),
+                v,
+                timestamp,
+            ))

+ 144 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/mmap_dict.py

@@ -0,0 +1,144 @@
+import json
+import mmap
+import os
+import struct
+
+_INITIAL_MMAP_SIZE = 1 << 20
+_pack_integer_func = struct.Struct(b'i').pack
+_pack_double_func = struct.Struct(b'd').pack
+_unpack_integer = struct.Struct(b'i').unpack_from
+_unpack_double = struct.Struct(b'd').unpack_from
+
+
+# struct.pack_into has atomicity issues because it will temporarily write 0 into
+# the mmap, resulting in false reads to 0 when experiencing a lot of writes.
+# Using direct assignment solves this issue.
+
+def _pack_double(data, pos, value):
+    data[pos:pos + 8] = _pack_double_func(value)
+
+
+def _pack_integer(data, pos, value):
+    data[pos:pos + 4] = _pack_integer_func(value)
+
+
+def _read_all_values(data, used=0):
+    """Yield (key, value, pos). No locking is performed."""
+
+    if used <= 0:
+        # If not valid `used` value is passed in, read it from the file.
+        used = _unpack_integer(data, 0)[0]
+
+    pos = 8
+
+    while pos < used:
+        encoded_len = _unpack_integer(data, pos)[0]
+        # check we are not reading beyond bounds
+        if encoded_len + pos > used:
+            raise RuntimeError('Read beyond file size detected, file is corrupted.')
+        pos += 4
+        encoded_key = data[pos : pos + encoded_len]
+        padded_len = encoded_len + (8 - (encoded_len + 4) % 8)
+        pos += padded_len
+        value = _unpack_double(data, pos)[0]
+        yield encoded_key.decode('utf-8'), value, pos
+        pos += 8
+
+
+class MmapedDict(object):
+    """A dict of doubles, backed by an mmapped file.
+
+    The file starts with a 4 byte int, indicating how much of it is used.
+    Then 4 bytes of padding.
+    There's then a number of entries, consisting of a 4 byte int which is the
+    size of the next field, a utf-8 encoded string key, padding to a 8 byte
+    alignment, and then a 8 byte float which is the value.
+
+    Not thread safe.
+    """
+
+    def __init__(self, filename, read_mode=False):
+        self._f = open(filename, 'rb' if read_mode else 'a+b')
+        self._fname = filename
+        capacity = os.fstat(self._f.fileno()).st_size
+        if capacity == 0:
+            self._f.truncate(_INITIAL_MMAP_SIZE)
+            capacity = _INITIAL_MMAP_SIZE
+        self._capacity = capacity
+        self._m = mmap.mmap(self._f.fileno(), self._capacity,
+                            access=mmap.ACCESS_READ if read_mode else mmap.ACCESS_WRITE)
+
+        self._positions = {}
+        self._used = _unpack_integer(self._m, 0)[0]
+        if self._used == 0:
+            self._used = 8
+            _pack_integer(self._m, 0, self._used)
+        else:
+            if not read_mode:
+                for key, _, pos in self._read_all_values():
+                    self._positions[key] = pos
+
+    @staticmethod
+    def read_all_values_from_file(filename):
+        with open(filename, 'rb') as infp:
+            # Read the first block of data, including the first 4 bytes which tell us
+            # how much of the file (which is preallocated to _INITIAL_MMAP_SIZE bytes) is occupied.
+            data = infp.read(65535)
+            used = _unpack_integer(data, 0)[0]
+            if used > len(data):  # Then read in the rest, if needed.
+                data += infp.read(used - len(data))
+        return _read_all_values(data, used)
+
+    def _init_value(self, key):
+        """Initialize a value. Lock must be held by caller."""
+        encoded = key.encode('utf-8')
+        # Pad to be 8-byte aligned.
+        padded = encoded + (b' ' * (8 - (len(encoded) + 4) % 8))
+        value = struct.pack('i{0}sd'.format(len(padded)).encode(), len(encoded), padded, 0.0)
+        while self._used + len(value) > self._capacity:
+            self._capacity *= 2
+            self._f.truncate(self._capacity)
+            self._m = mmap.mmap(self._f.fileno(), self._capacity)
+        self._m[self._used:self._used + len(value)] = value
+
+        # Update how much space we've used.
+        self._used += len(value)
+        _pack_integer(self._m, 0, self._used)
+        self._positions[key] = self._used - 8
+
+    def _read_all_values(self):
+        """Yield (key, value, pos). No locking is performed."""
+        return _read_all_values(data=self._m, used=self._used)
+
+    def read_all_values(self):
+        """Yield (key, value). No locking is performed."""
+        for k, v, _ in self._read_all_values():
+            yield k, v
+
+    def read_value(self, key):
+        if key not in self._positions:
+            self._init_value(key)
+        pos = self._positions[key]
+        # We assume that reading from an 8 byte aligned value is atomic
+        return _unpack_double(self._m, pos)[0]
+
+    def write_value(self, key, value):
+        if key not in self._positions:
+            self._init_value(key)
+        pos = self._positions[key]
+        # We assume that writing to an 8 byte aligned value is atomic
+        _pack_double(self._m, pos, value)
+
+    def close(self):
+        if self._f:
+            self._m.close()
+            self._m = None
+            self._f.close()
+            self._f = None
+
+
+def mmap_key(metric_name, name, labelnames, labelvalues):
+    """Format a key for use in the mmap file."""
+    # ensure labels are in consistent order for identity
+    labels = dict(zip(labelnames, labelvalues))
+    return json.dumps([metric_name, name, labels], sort_keys=True)

+ 161 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/multiprocess.py

@@ -0,0 +1,161 @@
+#!/usr/bin/python
+
+from __future__ import unicode_literals
+
+from collections import defaultdict
+import glob
+import json
+import os
+
+from .metrics_core import Metric
+from .mmap_dict import MmapedDict
+from .samples import Sample
+from .utils import floatToGoString
+
+try:  # Python3
+    FileNotFoundError
+except NameError:  # Python >= 2.5
+    FileNotFoundError = IOError
+
+MP_METRIC_HELP = 'Multiprocess metric'
+
+
+class MultiProcessCollector(object):
+    """Collector for files for multi-process mode."""
+
+    def __init__(self, registry, path=None):
+        if path is None:
+            path = os.environ.get('prometheus_multiproc_dir')
+        if not path or not os.path.isdir(path):
+            raise ValueError('env prometheus_multiproc_dir is not set or not a directory')
+        self._path = path
+        if registry:
+            registry.register(self)
+
+    @staticmethod
+    def merge(files, accumulate=True):
+        """Merge metrics from given mmap files.
+
+        By default, histograms are accumulated, as per prometheus wire format.
+        But if writing the merged data back to mmap files, use
+        accumulate=False to avoid compound accumulation.
+        """
+        metrics = MultiProcessCollector._read_metrics(files)
+        return MultiProcessCollector._accumulate_metrics(metrics, accumulate)
+
+    @staticmethod
+    def _read_metrics(files):
+        metrics = {}
+        key_cache = {}
+
+        def _parse_key(key):
+            val = key_cache.get(key)
+            if not val:
+                metric_name, name, labels = json.loads(key)
+                labels_key = tuple(sorted(labels.items()))
+                val = key_cache[key] = (metric_name, name, labels, labels_key)
+            return val
+
+        for f in files:
+            parts = os.path.basename(f).split('_')
+            typ = parts[0]
+            try:
+                file_values = MmapedDict.read_all_values_from_file(f)
+            except FileNotFoundError:
+                if typ == 'gauge' and parts[1] in ('liveall', 'livesum'):
+                    # Those files can disappear between the glob of collect
+                    # and now (via a mark_process_dead call) so don't fail if
+                    # the file is missing
+                    continue
+                raise
+            for key, value, pos in file_values:
+                metric_name, name, labels, labels_key = _parse_key(key)
+
+                metric = metrics.get(metric_name)
+                if metric is None:
+                    metric = Metric(metric_name, MP_METRIC_HELP, typ)
+                    metrics[metric_name] = metric
+
+                if typ == 'gauge':
+                    pid = parts[2][:-3]
+                    metric._multiprocess_mode = parts[1]
+                    metric.add_sample(name, labels_key + (('pid', pid),), value)
+                else:
+                    # The duplicates and labels are fixed in the next for.
+                    metric.add_sample(name, labels_key, value)
+        return metrics
+
+    @staticmethod
+    def _accumulate_metrics(metrics, accumulate):
+        for metric in metrics.values():
+            samples = defaultdict(float)
+            buckets = defaultdict(lambda: defaultdict(float))
+            samples_setdefault = samples.setdefault
+            for s in metric.samples:
+                name, labels, value, timestamp, exemplar = s
+                if metric.type == 'gauge':
+                    without_pid_key = (name, tuple([l for l in labels if l[0] != 'pid']))
+                    if metric._multiprocess_mode == 'min':
+                        current = samples_setdefault(without_pid_key, value)
+                        if value < current:
+                            samples[without_pid_key] = value
+                    elif metric._multiprocess_mode == 'max':
+                        current = samples_setdefault(without_pid_key, value)
+                        if value > current:
+                            samples[without_pid_key] = value
+                    elif metric._multiprocess_mode == 'livesum':
+                        samples[without_pid_key] += value
+                    else:  # all/liveall
+                        samples[(name, labels)] = value
+
+                elif metric.type == 'histogram':
+                    # A for loop with early exit is faster than a genexpr
+                    # or a listcomp that ends up building unnecessary things
+                    for l in labels:
+                        if l[0] == 'le':
+                            bucket_value = float(l[1])
+                            # _bucket
+                            without_le = tuple(l for l in labels if l[0] != 'le')
+                            buckets[without_le][bucket_value] += value
+                            break
+                    else:  # did not find the `le` key
+                        # _sum/_count
+                        samples[(name, labels)] += value
+                else:
+                    # Counter and Summary.
+                    samples[(name, labels)] += value
+
+            # Accumulate bucket values.
+            if metric.type == 'histogram':
+                for labels, values in buckets.items():
+                    acc = 0.0
+                    for bucket, value in sorted(values.items()):
+                        sample_key = (
+                            metric.name + '_bucket',
+                            labels + (('le', floatToGoString(bucket)),),
+                        )
+                        if accumulate:
+                            acc += value
+                            samples[sample_key] = acc
+                        else:
+                            samples[sample_key] = value
+                    if accumulate:
+                        samples[(metric.name + '_count', labels)] = acc
+
+            # Convert to correct sample format.
+            metric.samples = [Sample(name_, dict(labels), value) for (name_, labels), value in samples.items()]
+        return metrics.values()
+
+    def collect(self):
+        files = glob.glob(os.path.join(self._path, '*.db'))
+        return self.merge(files, accumulate=True)
+
+
+def mark_process_dead(pid, path=None):
+    """Do bookkeeping for when one process dies in a multi-process setup."""
+    if path is None:
+        path = os.environ.get('prometheus_multiproc_dir')
+    for f in glob.glob(os.path.join(path, 'gauge_livesum_{0}.db'.format(pid))):
+        os.remove(f)
+    for f in glob.glob(os.path.join(path, 'gauge_liveall_{0}.db'.format(pid))):
+        os.remove(f)

+ 0 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/openmetrics/__init__.py


+ 65 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/openmetrics/exposition.py

@@ -0,0 +1,65 @@
+#!/usr/bin/python
+
+from __future__ import unicode_literals
+
+from ..utils import floatToGoString
+
+CONTENT_TYPE_LATEST = str('application/openmetrics-text; version=0.0.1; charset=utf-8')
+"""Content type of the latest OpenMetrics text format"""
+
+
+def generate_latest(registry):
+    '''Returns the metrics from the registry in latest text format as a string.'''
+    output = []
+    for metric in registry.collect():
+        try:
+            mname = metric.name
+            output.append('# HELP {0} {1}\n'.format(
+                mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')))
+            output.append('# TYPE {0} {1}\n'.format(mname, metric.type))
+            if metric.unit:
+                output.append('# UNIT {0} {1}\n'.format(mname, metric.unit))
+            for s in metric.samples:
+                if s.labels:
+                    labelstr = '{{{0}}}'.format(','.join(
+                        ['{0}="{1}"'.format(
+                            k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
+                            for k, v in sorted(s.labels.items())]))
+                else:
+                    labelstr = ''
+                if s.exemplar:
+                    if metric.type not in ('histogram', 'gaugehistogram') or not s.name.endswith('_bucket'):
+                        raise ValueError("Metric {0} has exemplars, but is not a histogram bucket".format(metric.name))
+                    labels = '{{{0}}}'.format(','.join(
+                        ['{0}="{1}"'.format(
+                            k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
+                            for k, v in sorted(s.exemplar.labels.items())]))
+                    if s.exemplar.timestamp is not None:
+                        exemplarstr = ' # {0} {1} {2}'.format(
+                            labels,
+                            floatToGoString(s.exemplar.value),
+                            s.exemplar.timestamp,
+                        )
+                    else:
+                        exemplarstr = ' # {0} {1}'.format(
+                            labels,
+                            floatToGoString(s.exemplar.value),
+                        )
+                else:
+                    exemplarstr = ''
+                timestamp = ''
+                if s.timestamp is not None:
+                    timestamp = ' {0}'.format(s.timestamp)
+                output.append('{0}{1} {2}{3}{4}\n'.format(
+                    s.name,
+                    labelstr,
+                    floatToGoString(s.value),
+                    timestamp,
+                    exemplarstr,
+                ))
+        except Exception as exception:
+            exception.args = (exception.args or ('',)) + (metric,)
+            raise
+
+    output.append('# EOF\n')
+    return ''.join(output).encode('utf-8')

+ 575 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/openmetrics/parser.py

@@ -0,0 +1,575 @@
+#!/usr/bin/python
+
+from __future__ import unicode_literals
+
+import math
+import re
+
+from ..metrics_core import Metric, METRIC_LABEL_NAME_RE
+from ..samples import Exemplar, Sample, Timestamp
+from ..utils import floatToGoString
+
+try:
+    import StringIO
+except ImportError:
+    # Python 3
+    import io as StringIO
+
+
+def text_string_to_metric_families(text):
+    """Parse Openmetrics text format from a unicode string.
+
+    See text_fd_to_metric_families.
+    """
+    for metric_family in text_fd_to_metric_families(StringIO.StringIO(text)):
+        yield metric_family
+
+
+ESCAPE_SEQUENCES = {
+    '\\\\': '\\',
+    '\\n': '\n',
+    '\\"': '"',
+}
+
+
+def _replace_escape_sequence(match):
+    return ESCAPE_SEQUENCES[match.group(0)]
+
+
+ESCAPING_RE = re.compile(r'\\[\\n"]')
+
+
+def _replace_escaping(s):
+    return ESCAPING_RE.sub(_replace_escape_sequence, s)
+
+
+def _unescape_help(text):
+    result = []
+    slash = False
+
+    for char in text:
+        if slash:
+            if char == '\\':
+                result.append('\\')
+            elif char == '"':
+                result.append('"')
+            elif char == 'n':
+                result.append('\n')
+            else:
+                result.append('\\' + char)
+            slash = False
+        else:
+            if char == '\\':
+                slash = True
+            else:
+                result.append(char)
+
+    if slash:
+        result.append('\\')
+
+    return ''.join(result)
+
+
+def _parse_value(value):
+    value = ''.join(value)
+    if value != value.strip():
+        raise ValueError("Invalid value: {0!r}".format(value))
+    try:
+        return int(value)
+    except ValueError:
+        return float(value)
+
+
+def _parse_timestamp(timestamp):
+    timestamp = ''.join(timestamp)
+    if not timestamp:
+        return None
+    if timestamp != timestamp.strip():
+        raise ValueError("Invalid timestamp: {0!r}".format(timestamp))
+    try:
+        # Simple int.
+        return Timestamp(int(timestamp), 0)
+    except ValueError:
+        try:
+            # aaaa.bbbb. Nanosecond resolution supported.
+            parts = timestamp.split('.', 1)
+            return Timestamp(int(parts[0]), int(parts[1][:9].ljust(9, "0")))
+        except ValueError:
+            # Float.
+            ts = float(timestamp)
+            if math.isnan(ts) or math.isinf(ts):
+                raise ValueError("Invalid timestamp: {0!r}".format(timestamp))
+            return ts
+
+
+def _is_character_escaped(s, charpos):
+    num_bslashes = 0
+    while (charpos > num_bslashes and
+           s[charpos - 1 - num_bslashes] == '\\'):
+        num_bslashes += 1
+    return num_bslashes % 2 == 1
+
+
+def _parse_labels_with_state_machine(text):
+    # The { has already been parsed.
+    state = 'startoflabelname'
+    labelname = []
+    labelvalue = []
+    labels = {}
+    labels_len = 0
+
+    for char in text:
+        if state == 'startoflabelname':
+            if char == '}':
+                state = 'endoflabels'
+            else:
+                state = 'labelname'
+                labelname.append(char)
+        elif state == 'labelname':
+            if char == '=':
+                state = 'labelvaluequote'
+            else:
+                labelname.append(char)
+        elif state == 'labelvaluequote':
+            if char == '"':
+                state = 'labelvalue'
+            else:
+                raise ValueError("Invalid line: " + text)
+        elif state == 'labelvalue':
+            if char == '\\':
+                state = 'labelvalueslash'
+            elif char == '"':
+                if not METRIC_LABEL_NAME_RE.match(''.join(labelname)):
+                    raise ValueError("Invalid line: " + text)
+                labels[''.join(labelname)] = ''.join(labelvalue)
+                labelname = []
+                labelvalue = []
+                state = 'endoflabelvalue'
+            else:
+                labelvalue.append(char)
+        elif state == 'endoflabelvalue':
+            if char == ',':
+                state = 'labelname'
+            elif char == '}':
+                state = 'endoflabels'
+            else:
+                raise ValueError("Invalid line: " + text)
+        elif state == 'labelvalueslash':
+            state = 'labelvalue'
+            if char == '\\':
+                labelvalue.append('\\')
+            elif char == 'n':
+                labelvalue.append('\n')
+            elif char == '"':
+                labelvalue.append('"')
+            else:
+                labelvalue.append('\\' + char)
+        elif state == 'endoflabels':
+            if char == ' ':
+                break
+            else:
+                raise ValueError("Invalid line: " + text)
+        labels_len += 1
+    return labels, labels_len
+
+
+def _parse_labels(text):
+    labels = {}
+
+    # Raise error if we don't have valid labels
+    if text and "=" not in text:
+        raise ValueError
+
+    # Copy original labels
+    sub_labels = text
+    try:
+        # Process one label at a time
+        while sub_labels:
+            # The label name is before the equal
+            value_start = sub_labels.index("=")
+            label_name = sub_labels[:value_start]
+            sub_labels = sub_labels[value_start + 1:]
+
+            # Check for missing quotes 
+            if not sub_labels or sub_labels[0] != '"':
+                raise ValueError
+
+            # The first quote is guaranteed to be after the equal
+            value_substr = sub_labels[1:]
+
+            # Check for extra commas
+            if not label_name or label_name[0] == ',':
+                raise ValueError
+            if not value_substr or value_substr[-1] == ',':
+                raise ValueError
+
+            # Find the last unescaped quote
+            i = 0
+            while i < len(value_substr):
+                i = value_substr.index('"', i)
+                if not _is_character_escaped(value_substr[:i], i):
+                    break
+                i += 1
+
+            # The label value is inbetween the first and last quote
+            quote_end = i + 1
+            label_value = sub_labels[1:quote_end]
+            # Replace escaping if needed
+            if "\\" in label_value:
+                label_value = _replace_escaping(label_value)
+            labels[label_name] = label_value
+
+            # Remove the processed label from the sub-slice for next iteration
+            sub_labels = sub_labels[quote_end + 1:]
+            if sub_labels.startswith(","):
+                next_comma = 1
+            else:
+                next_comma = 0
+            sub_labels = sub_labels[next_comma:]
+
+            # Check for missing commas
+            if sub_labels and next_comma == 0:
+                raise ValueError
+            
+        return labels
+
+    except ValueError:
+        raise ValueError("Invalid labels: " + text)
+
+
+def _parse_sample(text):
+    # Detect the labels in the text
+    label_start = text.find("{")
+    if label_start == -1:
+        # We don't have labels
+        name_end = text.index(" ")
+        name = text[:name_end]
+        # Parse the remaining text after the name
+        remaining_text = text[name_end + 1:]
+        value, timestamp, exemplar = _parse_remaining_text(remaining_text)
+        return Sample(name, {}, value, timestamp, exemplar)
+    # The name is before the labels
+    name = text[:label_start]
+    seperator = " # "
+    if text.count(seperator) == 0:
+        # Line doesn't contain an exemplar
+        # We can use `rindex` to find `label_end`
+        label_end = text.rindex("}")
+        label = text[label_start + 1:label_end]
+        labels = _parse_labels(label)
+    else:
+        # Line potentially contains an exemplar
+        # Fallback to parsing labels with a state machine
+        labels, labels_len = _parse_labels_with_state_machine(text[label_start + 1:])
+        label_end = labels_len + len(name)      
+    # Parsing labels succeeded, continue parsing the remaining text
+    remaining_text = text[label_end + 2:]
+    value, timestamp, exemplar = _parse_remaining_text(remaining_text)
+    return Sample(name, labels, value, timestamp, exemplar)
+
+
+def _parse_remaining_text(text):
+    split_text = text.split(" ", 1)
+    val = _parse_value(split_text[0])
+    if len(split_text) == 1:
+        # We don't have timestamp or exemplar
+        return val, None, None  
+
+    timestamp = []
+    exemplar_value = []
+    exemplar_timestamp = []
+    exemplar_labels = None
+
+    state = 'timestamp'
+    text = split_text[1]
+
+    it = iter(text)
+    for char in it:
+        if state == 'timestamp':
+            if char == '#' and not timestamp:
+                state = 'exemplarspace'
+            elif char == ' ':
+                state = 'exemplarhash'
+            else:
+                timestamp.append(char)
+        elif state == 'exemplarhash':
+            if char == '#':
+                state = 'exemplarspace'
+            else:
+                raise ValueError("Invalid line: " + text)
+        elif state == 'exemplarspace':
+            if char == ' ':
+                state = 'exemplarstartoflabels'
+            else:
+                raise ValueError("Invalid line: " + text)
+        elif state == 'exemplarstartoflabels':
+            if char == '{':
+                label_start, label_end = text.index("{"), text.rindex("}")
+                exemplar_labels = _parse_labels(text[label_start + 1:label_end])
+                state = 'exemplarparsedlabels'
+            else:
+                raise ValueError("Invalid line: " + text)
+        elif state == 'exemplarparsedlabels':
+            if char == '}':
+                state = 'exemplarvaluespace'
+        elif state == 'exemplarvaluespace':
+            if char == ' ':
+                state = 'exemplarvalue'
+            else:
+                raise ValueError("Invalid line: " + text)
+        elif state == 'exemplarvalue':
+            if char == ' ' and not exemplar_value:
+                raise ValueError("Invalid line: " + text)
+            elif char == ' ':
+                state = 'exemplartimestamp'
+            else:
+                exemplar_value.append(char)
+        elif state == 'exemplartimestamp':
+            exemplar_timestamp.append(char)
+
+    # Trailing space after value.
+    if state == 'timestamp' and not timestamp:
+        raise ValueError("Invalid line: " + text)
+
+    # Trailing space after value.
+    if state == 'exemplartimestamp' and not exemplar_timestamp:
+        raise ValueError("Invalid line: " + text)
+
+    # Incomplete exemplar.
+    if state in ['exemplarhash', 'exemplarspace', 'exemplarstartoflabels', 'exemplarparsedlabels']:
+        raise ValueError("Invalid line: " + text)
+
+    ts = _parse_timestamp(timestamp)
+    exemplar = None
+    if exemplar_labels is not None:
+        exemplar_length = sum([len(k) + len(v) for k, v in exemplar_labels.items()])
+        if exemplar_length > 64:
+            raise ValueError("Exmplar labels are too long: " + text)
+        exemplar = Exemplar(
+            exemplar_labels,
+            _parse_value(exemplar_value),
+            _parse_timestamp(exemplar_timestamp),
+        )
+
+    return val, ts, exemplar
+
+
+def _group_for_sample(sample, name, typ):
+    if typ == 'info':
+        # We can't distinguish between groups for info metrics.
+        return {}
+    if typ == 'summary' and sample.name == name:
+        d = sample.labels.copy()
+        del d['quantile']
+        return d
+    if typ == 'stateset':
+        d = sample.labels.copy()
+        del d[name]
+        return d
+    if typ in ['histogram', 'gaugehistogram'] and sample.name == name + '_bucket':
+        d = sample.labels.copy()
+        del d['le']
+        return d
+    return sample.labels
+
+
+def _check_histogram(samples, name):
+    group = None
+    timestamp = None
+
+    def do_checks():
+        if bucket != float('+Inf'):
+            raise ValueError("+Inf bucket missing: " + name)
+        if count is not None and value != count:
+            raise ValueError("Count does not match +Inf value: " + name)
+
+    for s in samples:
+        suffix = s.name[len(name):]
+        g = _group_for_sample(s, name, 'histogram')
+        if g != group or s.timestamp != timestamp:
+            if group is not None:
+                do_checks()
+            count = None
+            bucket = -1
+            value = 0
+        group = g
+        timestamp = s.timestamp
+
+        if suffix == '_bucket':
+            b = float(s.labels['le'])
+            if b <= bucket:
+                raise ValueError("Buckets out of order: " + name)
+            if s.value < value:
+                raise ValueError("Bucket values out of order: " + name)
+            bucket = b
+            value = s.value
+        elif suffix in ['_count', '_gcount']:
+            count = s.value
+    if group is not None:
+        do_checks()
+
+
+def text_fd_to_metric_families(fd):
+    """Parse Prometheus text format from a file descriptor.
+
+    This is a laxer parser than the main Go parser,
+    so successful parsing does not imply that the parsed
+    text meets the specification.
+
+    Yields Metric's.
+    """
+    name = None
+    allowed_names = []
+    eof = False
+
+    seen_metrics = set()
+
+    def build_metric(name, documentation, typ, unit, samples):
+        if name in seen_metrics:
+            raise ValueError("Duplicate metric: " + name)
+        seen_metrics.add(name)
+        if typ is None:
+            typ = 'unknown'
+        if documentation is None:
+            documentation = ''
+        if unit is None:
+            unit = ''
+        if unit and not name.endswith("_" + unit):
+            raise ValueError("Unit does not match metric name: " + name)
+        if unit and typ in ['info', 'stateset']:
+            raise ValueError("Units not allowed for this metric type: " + name)
+        if typ in ['histogram', 'gaugehistogram']:
+            _check_histogram(samples, name)
+        metric = Metric(name, documentation, typ, unit)
+        # TODO: check labelvalues are valid utf8
+        metric.samples = samples
+        return metric
+
+    for line in fd:
+        if line[-1] == '\n':
+            line = line[:-1]
+
+        if eof:
+            raise ValueError("Received line after # EOF: " + line)
+
+        if line == '# EOF':
+            eof = True
+        elif line.startswith('#'):
+            parts = line.split(' ', 3)
+            if len(parts) < 4:
+                raise ValueError("Invalid line: " + line)
+            if parts[2] == name and samples:
+                raise ValueError("Received metadata after samples: " + line)
+            if parts[2] != name:
+                if name is not None:
+                    yield build_metric(name, documentation, typ, unit, samples)
+                # New metric
+                name = parts[2]
+                unit = None
+                typ = None
+                documentation = None
+                group = None
+                seen_groups = set()
+                group_timestamp = None
+                group_timestamp_samples = set()
+                samples = []
+                allowed_names = [parts[2]]
+
+            if parts[1] == 'HELP':
+                if documentation is not None:
+                    raise ValueError("More than one HELP for metric: " + line)
+                if len(parts) == 4:
+                    documentation = _unescape_help(parts[3])
+                elif len(parts) == 3:
+                    raise ValueError("Invalid line: " + line)
+            elif parts[1] == 'TYPE':
+                if typ is not None:
+                    raise ValueError("More than one TYPE for metric: " + line)
+                typ = parts[3]
+                if typ == 'untyped':
+                    raise ValueError("Invalid TYPE for metric: " + line)
+                allowed_names = {
+                    'counter': ['_total', '_created'],
+                    'summary': ['_count', '_sum', '', '_created'],
+                    'histogram': ['_count', '_sum', '_bucket', '_created'],
+                    'gaugehistogram': ['_gcount', '_gsum', '_bucket'],
+                    'info': ['_info'],
+                }.get(typ, [''])
+                allowed_names = [name + n for n in allowed_names]
+            elif parts[1] == 'UNIT':
+                if unit is not None:
+                    raise ValueError("More than one UNIT for metric: " + line)
+                unit = parts[3]
+            else:
+                raise ValueError("Invalid line: " + line)
+        else:
+            sample = _parse_sample(line)
+            if sample.name not in allowed_names:
+                if name is not None:
+                    yield build_metric(name, documentation, typ, unit, samples)
+                # Start an unknown metric.
+                name = sample.name
+                documentation = None
+                unit = None
+                typ = 'unknown'
+                samples = []
+                group = None
+                group_timestamp = None
+                group_timestamp_samples = set()
+                seen_groups = set()
+                allowed_names = [sample.name]
+
+            if typ == 'stateset' and name not in sample.labels:
+                raise ValueError("Stateset missing label: " + line)
+            if (typ in ['histogram', 'gaugehistogram'] and name + '_bucket' == sample.name
+                    and (float(sample.labels.get('le', -1)) < 0
+                         or sample.labels['le'] != floatToGoString(sample.labels['le']))):
+                raise ValueError("Invalid le label: " + line)
+            if (typ == 'summary' and name == sample.name
+                    and (not (0 <= float(sample.labels.get('quantile', -1)) <= 1)
+                         or sample.labels['quantile'] != floatToGoString(sample.labels['quantile']))):
+                raise ValueError("Invalid quantile label: " + line)
+
+            g = tuple(sorted(_group_for_sample(sample, name, typ).items()))
+            if group is not None and g != group and g in seen_groups:
+                raise ValueError("Invalid metric grouping: " + line)
+            if group is not None and g == group:
+                if (sample.timestamp is None) != (group_timestamp is None):
+                    raise ValueError("Mix of timestamp presence within a group: " + line)
+                if group_timestamp is not None and group_timestamp > sample.timestamp and typ != 'info':
+                    raise ValueError("Timestamps went backwards within a group: " + line)
+            else:
+                group_timestamp_samples = set()
+
+            series_id = (sample.name, tuple(sorted(sample.labels.items())))
+            if sample.timestamp != group_timestamp or series_id not in group_timestamp_samples:
+                # Not a duplicate due to timestamp truncation.
+                samples.append(sample)
+            group_timestamp_samples.add(series_id)
+
+            group = g
+            group_timestamp = sample.timestamp
+            seen_groups.add(g)
+
+            if typ == 'stateset' and sample.value not in [0, 1]:
+                raise ValueError("Stateset samples can only have values zero and one: " + line)
+            if typ == 'info' and sample.value != 1:
+                raise ValueError("Info samples can only have value one: " + line)
+            if typ == 'summary' and name == sample.name and sample.value < 0:
+                raise ValueError("Quantile values cannot be negative: " + line)
+            if sample.name[len(name):] in ['_total', '_sum', '_count', '_bucket', '_gcount', '_gsum'] and math.isnan(
+                    sample.value):
+                raise ValueError("Counter-like samples cannot be NaN: " + line)
+            if sample.name[len(name):] in ['_total', '_sum', '_count', '_bucket', '_gcount',
+                                           '_gsum'] and sample.value < 0:
+                raise ValueError("Counter-like samples cannot be negative: " + line)
+            if sample.exemplar and not (
+                    typ in ['histogram', 'gaugehistogram']
+                    and sample.name.endswith('_bucket')):
+                raise ValueError("Invalid line only histogram/gaugehistogram buckets can have exemplars: " + line)
+
+    if name is not None:
+        yield build_metric(name, documentation, typ, unit, samples)
+
+    if not eof:
+        raise ValueError("Missing # EOF at end")

+ 232 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/parser.py

@@ -0,0 +1,232 @@
+#!/usr/bin/python
+
+from __future__ import unicode_literals
+
+import re
+
+from .metrics_core import Metric
+from .samples import Sample
+
+try:
+    import StringIO
+except ImportError:
+    # Python 3
+    import io as StringIO
+
+
+def text_string_to_metric_families(text):
+    """Parse Prometheus text format from a unicode string.
+
+    See text_fd_to_metric_families.
+    """
+    for metric_family in text_fd_to_metric_families(StringIO.StringIO(text)):
+        yield metric_family
+
+
+ESCAPE_SEQUENCES = {
+    '\\\\': '\\',
+    '\\n': '\n',
+    '\\"': '"',
+}
+
+
+def replace_escape_sequence(match):
+    return ESCAPE_SEQUENCES[match.group(0)]
+
+
+HELP_ESCAPING_RE = re.compile(r'\\[\\n]')
+ESCAPING_RE = re.compile(r'\\[\\n"]')
+
+
+def _replace_help_escaping(s):
+    return HELP_ESCAPING_RE.sub(replace_escape_sequence, s)
+
+
+def _replace_escaping(s):
+    return ESCAPING_RE.sub(replace_escape_sequence, s)
+
+
+def _is_character_escaped(s, charpos):
+    num_bslashes = 0
+    while (charpos > num_bslashes and
+           s[charpos - 1 - num_bslashes] == '\\'):
+        num_bslashes += 1
+    return num_bslashes % 2 == 1
+
+
+def _parse_labels(labels_string):
+    labels = {}
+    # Return if we don't have valid labels
+    if "=" not in labels_string:
+        return labels
+
+    escaping = False
+    if "\\" in labels_string:
+        escaping = True
+
+    # Copy original labels
+    sub_labels = labels_string
+    try:
+        # Process one label at a time
+        while sub_labels:
+            # The label name is before the equal
+            value_start = sub_labels.index("=")
+            label_name = sub_labels[:value_start]
+            sub_labels = sub_labels[value_start + 1:].lstrip()
+            # Find the first quote after the equal
+            quote_start = sub_labels.index('"') + 1
+            value_substr = sub_labels[quote_start:]
+
+            # Find the last unescaped quote
+            i = 0
+            while i < len(value_substr):
+                i = value_substr.index('"', i)
+                if not _is_character_escaped(value_substr, i):
+                    break
+                i += 1
+
+            # The label value is inbetween the first and last quote
+            quote_end = i + 1
+            label_value = sub_labels[quote_start:quote_end]
+            # Replace escaping if needed
+            if escaping:
+                label_value = _replace_escaping(label_value)
+            labels[label_name.strip()] = label_value
+
+            # Remove the processed label from the sub-slice for next iteration
+            sub_labels = sub_labels[quote_end + 1:]
+            next_comma = sub_labels.find(",") + 1
+            sub_labels = sub_labels[next_comma:].lstrip()
+
+        return labels
+
+    except ValueError:
+        raise ValueError("Invalid labels: %s" % labels_string)
+
+
+# If we have multiple values only consider the first
+def _parse_value(s):
+    s = s.lstrip()
+    separator = " "
+    if separator not in s:
+        separator = "\t"
+    i = s.find(separator)
+    if i == -1:
+        return s
+    return s[:i]
+
+
+def _parse_sample(text):
+    # Detect the labels in the text
+    try:
+        label_start, label_end = text.index("{"), text.rindex("}")
+        # The name is before the labels
+        name = text[:label_start].strip()
+        # We ignore the starting curly brace
+        label = text[label_start + 1:label_end]
+        # The value is after the label end (ignoring curly brace and space)
+        value = float(_parse_value(text[label_end + 2:]))
+        return Sample(name, _parse_labels(label), value)
+
+    # We don't have labels
+    except ValueError:
+        # Detect what separator is used
+        separator = " "
+        if separator not in text:
+            separator = "\t"
+        name_end = text.index(separator)
+        name = text[:name_end]
+        # The value is after the name
+        value = float(_parse_value(text[name_end:]))
+        return Sample(name, {}, value)
+
+
+def text_fd_to_metric_families(fd):
+    """Parse Prometheus text format from a file descriptor.
+
+    This is a laxer parser than the main Go parser,
+    so successful parsing does not imply that the parsed
+    text meets the specification.
+
+    Yields Metric's.
+    """
+    name = ''
+    documentation = ''
+    typ = 'untyped'
+    samples = []
+    allowed_names = []
+
+    def build_metric(name, documentation, typ, samples):
+        # Munge counters into OpenMetrics representation
+        # used internally.
+        if typ == 'counter':
+            if name.endswith('_total'):
+                name = name[:-6]
+            else:
+                new_samples = []
+                for s in samples:
+                    new_samples.append(Sample(s[0] + '_total', *s[1:]))
+                    samples = new_samples
+        metric = Metric(name, documentation, typ)
+        metric.samples = samples
+        return metric
+
+    for line in fd:
+        line = line.strip()
+
+        if line.startswith('#'):
+            parts = line.split(None, 3)
+            if len(parts) < 2:
+                continue
+            if parts[1] == 'HELP':
+                if parts[2] != name:
+                    if name != '':
+                        yield build_metric(name, documentation, typ, samples)
+                    # New metric
+                    name = parts[2]
+                    typ = 'untyped'
+                    samples = []
+                    allowed_names = [parts[2]]
+                if len(parts) == 4:
+                    documentation = _replace_help_escaping(parts[3])
+                else:
+                    documentation = ''
+            elif parts[1] == 'TYPE':
+                if parts[2] != name:
+                    if name != '':
+                        yield build_metric(name, documentation, typ, samples)
+                    # New metric
+                    name = parts[2]
+                    documentation = ''
+                    samples = []
+                typ = parts[3]
+                allowed_names = {
+                    'counter': [''],
+                    'gauge': [''],
+                    'summary': ['_count', '_sum', ''],
+                    'histogram': ['_count', '_sum', '_bucket'],
+                }.get(typ, [''])
+                allowed_names = [name + n for n in allowed_names]
+            else:
+                # Ignore other comment tokens
+                pass
+        elif line == '':
+            # Ignore blank lines
+            pass
+        else:
+            sample = _parse_sample(line)
+            if sample.name not in allowed_names:
+                if name != '':
+                    yield build_metric(name, documentation, typ, samples)
+                # New metric, yield immediately as untyped singleton
+                name = ''
+                documentation = ''
+                typ = 'untyped'
+                samples = []
+                allowed_names = []
+                yield build_metric(sample[0], documentation, typ, [sample])
+            else:
+                samples.append(sample)
+
+    if name != '':
+        yield build_metric(name, documentation, typ, samples)

+ 59 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/platform_collector.py

@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+# -*- coding: utf-8
+from __future__ import unicode_literals
+
+import platform as pf
+
+from .metrics_core import GaugeMetricFamily
+from .registry import REGISTRY
+
+
+class PlatformCollector(object):
+    """Collector for python platform information"""
+
+    def __init__(self, registry=REGISTRY, platform=None):
+        self._platform = pf if platform is None else platform
+        info = self._info()
+        system = self._platform.system()
+        if system == "Java":
+            info.update(self._java())
+        self._metrics = [
+            self._add_metric("python_info", "Python platform information", info)
+        ]
+        if registry:
+            registry.register(self)
+
+    def collect(self):
+        return self._metrics
+
+    @staticmethod
+    def _add_metric(name, documentation, data):
+        labels = data.keys()
+        values = [data[k] for k in labels]
+        g = GaugeMetricFamily(name, documentation, labels=labels)
+        g.add_metric(values, 1)
+        return g
+
+    def _info(self):
+        major, minor, patchlevel = self._platform.python_version_tuple()
+        return {
+            "version": self._platform.python_version(),
+            "implementation": self._platform.python_implementation(),
+            "major": major,
+            "minor": minor,
+            "patchlevel": patchlevel
+        }
+
+    def _java(self):
+        java_version, _, vminfo, osinfo = self._platform.java_ver()
+        vm_name, vm_release, vm_vendor = vminfo
+        return {
+            "jvm_version": java_version,
+            "jvm_release": vm_release,
+            "jvm_vendor": vm_vendor,
+            "jvm_name": vm_name
+        }
+
+
+PLATFORM_COLLECTOR = PlatformCollector()
+"""PlatformCollector in default Registry REGISTRY"""

+ 98 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/process_collector.py

@@ -0,0 +1,98 @@
+#!/usr/bin/python
+
+from __future__ import unicode_literals
+
+import os
+
+from .metrics_core import CounterMetricFamily, GaugeMetricFamily
+from .registry import REGISTRY
+
+try:
+    import resource
+
+    _PAGESIZE = resource.getpagesize()
+except ImportError:
+    # Not Unix
+    _PAGESIZE = 4096
+
+
+class ProcessCollector(object):
+    """Collector for Standard Exports such as cpu and memory."""
+
+    def __init__(self, namespace='', pid=lambda: 'self', proc='/proc', registry=REGISTRY):
+        self._namespace = namespace
+        self._pid = pid
+        self._proc = proc
+        if namespace:
+            self._prefix = namespace + '_process_'
+        else:
+            self._prefix = 'process_'
+        self._ticks = 100.0
+        try:
+            self._ticks = os.sysconf('SC_CLK_TCK')
+        except (ValueError, TypeError, AttributeError):
+            pass
+
+        # This is used to test if we can access /proc.
+        self._btime = 0
+        try:
+            self._btime = self._boot_time()
+        except IOError:
+            pass
+        if registry:
+            registry.register(self)
+
+    def _boot_time(self):
+        with open(os.path.join(self._proc, 'stat'), 'rb') as stat:
+            for line in stat:
+                if line.startswith(b'btime '):
+                    return float(line.split()[1])
+
+    def collect(self):
+        if not self._btime:
+            return []
+
+        pid = os.path.join(self._proc, str(self._pid()).strip())
+
+        result = []
+        try:
+            with open(os.path.join(pid, 'stat'), 'rb') as stat:
+                parts = (stat.read().split(b')')[-1].split())
+
+            vmem = GaugeMetricFamily(self._prefix + 'virtual_memory_bytes',
+                                     'Virtual memory size in bytes.', value=float(parts[20]))
+            rss = GaugeMetricFamily(self._prefix + 'resident_memory_bytes', 'Resident memory size in bytes.',
+                                    value=float(parts[21]) * _PAGESIZE)
+            start_time_secs = float(parts[19]) / self._ticks
+            start_time = GaugeMetricFamily(self._prefix + 'start_time_seconds',
+                                           'Start time of the process since unix epoch in seconds.',
+                                           value=start_time_secs + self._btime)
+            utime = float(parts[11]) / self._ticks
+            stime = float(parts[12]) / self._ticks
+            cpu = CounterMetricFamily(self._prefix + 'cpu_seconds_total',
+                                      'Total user and system CPU time spent in seconds.',
+                                      value=utime + stime)
+            result.extend([vmem, rss, start_time, cpu])
+        except IOError:
+            pass
+
+        try:
+            with open(os.path.join(pid, 'limits'), 'rb') as limits:
+                for line in limits:
+                    if line.startswith(b'Max open file'):
+                        max_fds = GaugeMetricFamily(self._prefix + 'max_fds',
+                                                    'Maximum number of open file descriptors.',
+                                                    value=float(line.split()[3]))
+                        break
+            open_fds = GaugeMetricFamily(self._prefix + 'open_fds',
+                                         'Number of open file descriptors.',
+                                         len(os.listdir(os.path.join(pid, 'fd'))))
+            result.extend([open_fds, max_fds])
+        except (IOError, OSError):
+            pass
+
+        return result
+
+
+PROCESS_COLLECTOR = ProcessCollector()
+"""Default ProcessCollector in default Registry REGISTRY."""

+ 123 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/registry.py

@@ -0,0 +1,123 @@
+import copy
+from threading import Lock
+
+from .metrics_core import Metric
+
+
+class CollectorRegistry(object):
+    """Metric collector registry.
+
+    Collectors must have a no-argument method 'collect' that returns a list of
+    Metric objects. The returned metrics should be consistent with the Prometheus
+    exposition formats.
+    """
+
+    def __init__(self, auto_describe=False):
+        self._collector_to_names = {}
+        self._names_to_collectors = {}
+        self._auto_describe = auto_describe
+        self._lock = Lock()
+
+    def register(self, collector):
+        """Add a collector to the registry."""
+        with self._lock:
+            names = self._get_names(collector)
+            duplicates = set(self._names_to_collectors).intersection(names)
+            if duplicates:
+                raise ValueError(
+                    'Duplicated timeseries in CollectorRegistry: {0}'.format(
+                        duplicates))
+            for name in names:
+                self._names_to_collectors[name] = collector
+            self._collector_to_names[collector] = names
+
+    def unregister(self, collector):
+        """Remove a collector from the registry."""
+        with self._lock:
+            for name in self._collector_to_names[collector]:
+                del self._names_to_collectors[name]
+            del self._collector_to_names[collector]
+
+    def _get_names(self, collector):
+        """Get names of timeseries the collector produces."""
+        desc_func = None
+        # If there's a describe function, use it.
+        try:
+            desc_func = collector.describe
+        except AttributeError:
+            pass
+        # Otherwise, if auto describe is enabled use the collect function.
+        if not desc_func and self._auto_describe:
+            desc_func = collector.collect
+
+        if not desc_func:
+            return []
+
+        result = []
+        type_suffixes = {
+            'counter': ['_total', '_created'],
+            'summary': ['', '_sum', '_count', '_created'],
+            'histogram': ['_bucket', '_sum', '_count', '_created'],
+            'gaugehistogram': ['_bucket', '_gsum', '_gcount'],
+            'info': ['_info'],
+        }
+        for metric in desc_func():
+            for suffix in type_suffixes.get(metric.type, ['']):
+                result.append(metric.name + suffix)
+        return result
+
+    def collect(self):
+        """Yields metrics from the collectors in the registry."""
+        collectors = None
+        with self._lock:
+            collectors = copy.copy(self._collector_to_names)
+        for collector in collectors:
+            for metric in collector.collect():
+                yield metric
+
+    def restricted_registry(self, names):
+        """Returns object that only collects some metrics.
+
+        Returns an object which upon collect() will return
+        only samples with the given names.
+
+        Intended usage is:
+            generate_latest(REGISTRY.restricted_registry(['a_timeseries']))
+
+        Experimental."""
+        names = set(names)
+        collectors = set()
+        with self._lock:
+            for name in names:
+                if name in self._names_to_collectors:
+                    collectors.add(self._names_to_collectors[name])
+        metrics = []
+        for collector in collectors:
+            for metric in collector.collect():
+                samples = [s for s in metric.samples if s[0] in names]
+                if samples:
+                    m = Metric(metric.name, metric.documentation, metric.type)
+                    m.samples = samples
+                    metrics.append(m)
+
+        class RestrictedRegistry(object):
+            def collect(self):
+                return metrics
+
+        return RestrictedRegistry()
+
+    def get_sample_value(self, name, labels=None):
+        """Returns the sample value, or None if not found.
+
+        This is inefficient, and intended only for use in unittests.
+        """
+        if labels is None:
+            labels = {}
+        for metric in self.collect():
+            for s in metric.samples:
+                if s.name == name and s.labels == labels:
+                    return s.value
+        return None
+
+
+REGISTRY = CollectorRegistry(auto_describe=True)

+ 43 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/samples.py

@@ -0,0 +1,43 @@
+from collections import namedtuple
+
+
+class Timestamp(object):
+    """A nanosecond-resolution timestamp."""
+
+    def __init__(self, sec, nsec):
+        if nsec < 0 or nsec >= 1e9:
+            raise ValueError("Invalid value for nanoseconds in Timestamp: {0}".format(nsec))
+        if sec < 0:
+            nsec = -nsec
+        self.sec = int(sec)
+        self.nsec = int(nsec)
+
+    def __str__(self):
+        return "{0}.{1:09d}".format(self.sec, self.nsec)
+
+    def __repr__(self):
+        return "Timestamp({0}, {1})".format(self.sec, self.nsec)
+
+    def __float__(self):
+        return float(self.sec) + float(self.nsec) / 1e9
+
+    def __eq__(self, other):
+        return type(self) == type(other) and self.sec == other.sec and self.nsec == other.nsec
+
+    def __ne__(self, other):
+        return not self == other
+
+    def __gt__(self, other):
+        return self.sec > other.sec or self.nsec > other.nsec
+
+
+# Timestamp and exemplar are optional.
+# Value can be an int or a float.
+# Timestamp can be a float containing a unixtime in seconds,
+# a Timestamp object, or None.
+# Exemplar can be an Exemplar object, or None.
+Sample = namedtuple('Sample', ['name', 'labels', 'value', 'timestamp', 'exemplar'])
+Sample.__new__.__defaults__ = (None, None)
+
+Exemplar = namedtuple('Exemplar', ['labels', 'value', 'timestamp'])
+Exemplar.__new__.__defaults__ = (None,)

+ 3 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/twisted/__init__.py

@@ -0,0 +1,3 @@
+from ._exposition import MetricsResource
+
+__all__ = ['MetricsResource']

+ 20 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/twisted/_exposition.py

@@ -0,0 +1,20 @@
+from __future__ import absolute_import, unicode_literals
+
+from twisted.web.resource import Resource
+
+from .. import exposition, REGISTRY
+
+
+class MetricsResource(Resource):
+    """
+    Twisted ``Resource`` that serves prometheus metrics.
+    """
+    isLeaf = True
+
+    def __init__(self, registry=REGISTRY):
+        self.registry = registry
+
+    def render_GET(self, request):
+        encoder, content_type = exposition.choose_encoder(request.getHeader('Accept'))
+        request.setHeader(b'Content-Type', content_type.encode('ascii'))
+        return encoder(self.registry)

+ 23 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/utils.py

@@ -0,0 +1,23 @@
+import math
+
+INF = float("inf")
+MINUS_INF = float("-inf")
+
+
+def floatToGoString(d):
+    d = float(d)
+    if d == INF:
+        return '+Inf'
+    elif d == MINUS_INF:
+        return '-Inf'
+    elif math.isnan(d):
+        return 'NaN'
+    else:
+        s = repr(d)
+        dot = s.find('.')
+        # Go switches to exponents sooner than Python.
+        # We only need to care about positive values for le/quantile.
+        if d > 0 and dot > 6:
+            mantissa = '{0}.{1}{2}'.format(s[0], s[1:dot], s[dot + 1:]).rstrip('0.')
+            return '{0}e+0{1}'.format(mantissa, dot - 1)
+        return s

+ 110 - 0
desktop/core/ext-py/prometheus_client-0.7.1/prometheus_client/values.py

@@ -0,0 +1,110 @@
+from __future__ import unicode_literals
+
+import os
+from threading import Lock
+
+from .mmap_dict import mmap_key, MmapedDict
+
+
+class MutexValue(object):
+    """A float protected by a mutex."""
+
+    _multiprocess = False
+
+    def __init__(self, typ, metric_name, name, labelnames, labelvalues, **kwargs):
+        self._value = 0.0
+        self._lock = Lock()
+
+    def inc(self, amount):
+        with self._lock:
+            self._value += amount
+
+    def set(self, value):
+        with self._lock:
+            self._value = value
+
+    def get(self):
+        with self._lock:
+            return self._value
+
+
+def MultiProcessValue(_pidFunc=os.getpid):
+    files = {}
+    values = []
+    pid = {'value': _pidFunc()}
+    # Use a single global lock when in multi-processing mode
+    # as we presume this means there is no threading going on.
+    # This avoids the need to also have mutexes in __MmapDict.
+    lock = Lock()
+
+    class MmapedValue(object):
+        """A float protected by a mutex backed by a per-process mmaped file."""
+
+        _multiprocess = True
+
+        def __init__(self, typ, metric_name, name, labelnames, labelvalues, multiprocess_mode='', **kwargs):
+            self._params = typ, metric_name, name, labelnames, labelvalues, multiprocess_mode
+            with lock:
+                self.__check_for_pid_change()
+                self.__reset()
+                values.append(self)
+
+        def __reset(self):
+            typ, metric_name, name, labelnames, labelvalues, multiprocess_mode = self._params
+            if typ == 'gauge':
+                file_prefix = typ + '_' + multiprocess_mode
+            else:
+                file_prefix = typ
+            if file_prefix not in files:
+                filename = os.path.join(
+                    os.environ['prometheus_multiproc_dir'],
+                    '{0}_{1}.db'.format(file_prefix, pid['value']))
+
+                files[file_prefix] = MmapedDict(filename)
+            self._file = files[file_prefix]
+            self._key = mmap_key(metric_name, name, labelnames, labelvalues)
+            self._value = self._file.read_value(self._key)
+
+        def __check_for_pid_change(self):
+            actual_pid = _pidFunc()
+            if pid['value'] != actual_pid:
+                pid['value'] = actual_pid
+                # There has been a fork(), reset all the values.
+                for f in files.values():
+                    f.close()
+                files.clear()
+                for value in values:
+                    value.__reset()
+
+        def inc(self, amount):
+            with lock:
+                self.__check_for_pid_change()
+                self._value += amount
+                self._file.write_value(self._key, self._value)
+
+        def set(self, value):
+            with lock:
+                self.__check_for_pid_change()
+                self._value = value
+                self._file.write_value(self._key, self._value)
+
+        def get(self):
+            with lock:
+                self.__check_for_pid_change()
+                return self._value
+
+    return MmapedValue
+
+
+def get_value_class():
+    # Should we enable multi-process mode?
+    # This needs to be chosen before the first metric is constructed,
+    # and as that may be in some arbitrary library the user/admin has
+    # no control over we use an environment variable.
+    if 'prometheus_multiproc_dir' in os.environ:
+        return MultiProcessValue()
+    else:
+        return MutexValue
+
+
+ValueClass = get_value_class()

+ 4 - 0
desktop/core/ext-py/prometheus_client-0.7.1/setup.cfg

@@ -0,0 +1,4 @@
+[egg_info]
+tag_build = 
+tag_date = 0
+

+ 44 - 0
desktop/core/ext-py/prometheus_client-0.7.1/setup.py

@@ -0,0 +1,44 @@
+from setuptools import setup
+
+setup(
+    name="prometheus_client",
+    version="0.7.1",
+    author="Brian Brazil",
+    author_email="brian.brazil@robustperception.io",
+    description="Python client for the Prometheus monitoring system.",
+    long_description=(
+        "See https://github.com/prometheus/client_python/blob/master/README.md"
+        " for documentation."),
+    license="Apache Software License 2.0",
+    keywords="prometheus monitoring instrumentation client",
+    url="https://github.com/prometheus/client_python",
+    packages=[
+        'prometheus_client',
+        'prometheus_client.bridge',
+        'prometheus_client.openmetrics',
+        'prometheus_client.twisted',
+    ],
+    extras_require={
+        'twisted': ['twisted'],
+    },
+    test_suite="tests",
+    classifiers=[
+        "Development Status :: 4 - Beta",
+        "Intended Audience :: Developers",
+        "Intended Audience :: Information Technology",
+        "Intended Audience :: System Administrators",
+        "Programming Language :: Python",
+        "Programming Language :: Python :: 2",
+        "Programming Language :: Python :: 2.6",
+        "Programming Language :: Python :: 2.7",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.4",
+        "Programming Language :: Python :: 3.5",
+        "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: Implementation :: CPython",
+        "Programming Language :: Python :: Implementation :: PyPy",
+        "Topic :: System :: Monitoring",
+        "License :: OSI Approved :: Apache Software License",
+    ],
+)