Ver código fonte

[desktop] Initial support for metrics

Currently tracks:

* thread count
* multiprocessing processes (if we ever end up using them)
* gc
* logged in users
* active requests
* requests that ended because of an exception
* response times
Erick Tryzelaar 10 anos atrás
pai
commit
21d2556574

+ 5 - 5
desktop/Makefile

@@ -41,14 +41,14 @@ include $(ROOT)/Makefile.vars.priv
 
 APPS := core \
 	libs/hadoop \
+	libs/indexer \
+	libs/liboauth \
 	libs/liboozie \
-	libs/libsaml \
-	libs/librdbms \
 	libs/libopenid \
-	libs/liboauth \
-	libs/libsolr \
+	libs/librdbms \
+	libs/libsaml \
 	libs/libsentry \
-	libs/indexer
+	libs/libsolr
 
 .PHONY: default
 default:: hue syncdb

+ 2 - 2
desktop/core/src/desktop/lib/django_util.py

@@ -462,10 +462,10 @@ class JsonResponse(HttpResponse):
       to ``True``.
     """
 
-    def __init__(self, data, encoder=DjangoJSONEncoder, safe=True, **kwargs):
+    def __init__(self, data, encoder=DjangoJSONEncoder, safe=True, indent=None, **kwargs):
         if safe and not isinstance(data, dict):
             raise TypeError('In order to allow non-dict objects to be '
                 'serialized set the safe parameter to False')
         kwargs.setdefault('content_type', 'application/json')
-        data = json.dumps(data, cls=encoder)
+        data = json.dumps(data, cls=encoder, indent=indent)
         super(JsonResponse, self).__init__(content=data, **kwargs)

+ 17 - 0
desktop/core/src/desktop/lib/metrics/__init__.py

@@ -0,0 +1,17 @@
+# Licensed to Cloudera, Inc. under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  Cloudera, Inc. licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from desktop.lib.metrics.registry import global_registry

+ 81 - 0
desktop/core/src/desktop/lib/metrics/registry.py

@@ -0,0 +1,81 @@
+# Licensed to Cloudera, Inc. under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  Cloudera, Inc. licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+All Hue metrics should be defined in the APP/metrics.py file so they are discoverable.
+"""
+
+import pyformance
+
+
+class MetricsRegistry(object):
+  def __init__(self, registry=None):
+    if registry is None:
+      registry = pyformance.global_registry()
+    self._registry = registry
+    self._schemas = []
+
+  def _register_schema(self, schema):
+    self._schemas.append(schema)
+
+  def counter(self, name, **kwargs):
+    self._schemas.append(MetricDefinition('counter', name, **kwargs))
+    return self._registry.counter(name)
+
+  def histogram(self, name, **kwargs):
+    self._schemas.append(MetricDefinition('histogram', name, **kwargs))
+    return self._registry.histogram(name)
+
+  def gauge(self, name, gauge=None, default=float('nan'), **kwargs):
+    self._schemas.append(MetricDefinition('gauge', name, **kwargs))
+    return self._registry.gauge(name, gauge, default)
+
+  def gauge_callback(self, name, callback, default=float('nan'), **kwargs):
+    self._schemas.append(MetricDefinition('gauge', name, **kwargs))
+    return self._registry.gauge(name, pyformance.meters.CallbackGauge(callback), default)
+
+  def meter(self, name, **kwargs):
+    self._schemas.append(MetricDefinition('meter', name, **kwargs))
+    return self._registry.meter(name)
+
+  def timer(self, name, **kwargs):
+    self._schemas.append(MetricDefinition('timer', name, **kwargs))
+    return self._registry.timer(name)
+
+  def dump_metrics(self):
+    return self._registry.dump_metrics()
+
+
+class MetricDefinition(object):
+  def __init__(self, metric_type, name, label,
+      description=None,
+      numerator=None,
+      denominator=None,
+      context=None):
+    self.metric_type = metric_type
+    self.name = name
+    self.label = label
+    self.description = description
+    self.numerator = numerator
+    self.denominator = denominator
+    self.context = context
+
+
+_global_registry = MetricsRegistry()
+
+
+def global_registry():
+  return _global_registry

+ 24 - 0
desktop/core/src/desktop/lib/metrics/urls.py

@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+# Licensed to Cloudera, Inc. under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  Cloudera, Inc. licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from django.conf.urls import url
+
+from desktop.lib.metrics import views
+
+urlpatterns = [
+  url(r'^$', views.index, name='index'),
+]

+ 37 - 0
desktop/core/src/desktop/lib/metrics/views.py

@@ -0,0 +1,37 @@
+#!/usr/bin/env python
+# Licensed to Cloudera, Inc. under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  Cloudera, Inc. licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import datetime
+import json
+
+from django.views.decorators.http import require_GET
+
+from desktop.lib.django_util import JsonResponse, render, login_notrequired
+from desktop.lib.metrics.registry import global_registry
+
+@require_GET
+def index(request):
+  if request.GET.get('pretty') == 'true':
+    indent = 2
+  else:
+    indent = None
+
+  rep = {
+      'timestamp': datetime.datetime.utcnow().isoformat(),
+      'metric': global_registry().dump_metrics(),
+  }
+  return JsonResponse(rep, indent=indent)

+ 155 - 0
desktop/core/src/desktop/metrics.py

@@ -0,0 +1,155 @@
+# Licensed to Cloudera, Inc. under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  Cloudera, Inc. licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import gc
+import multiprocessing
+import threading
+
+from django.contrib.auth.models import User
+from django.contrib.auth.signals import user_logged_in, user_logged_out
+from django.db.models.signals import post_save, post_delete
+from django.dispatch import receiver
+
+from desktop.lib.metrics import global_registry
+
+global_registry().gauge_callback(
+    name='python.threads.count',
+    callback=lambda: len(threading.enumerate()),
+    label='Thread count',
+    description='Number of threads',
+)
+
+global_registry().gauge_callback(
+    name='python.threads.active',
+    callback=lambda: threading.active_count(),
+    label='Active thread count',
+    description='Number of active threads',
+)
+
+global_registry().gauge_callback(
+    name='python.threads.daemon',
+    callback=lambda: sum(1 for thread in threading.enumerate() if thread.isDaemon()),
+    label='Daemon thread count',
+    description='Number of daemon threads',
+)
+
+# ------------------------------------------------------------------------------
+
+global_registry().gauge_callback(
+    name='python.multiprocessing.count',
+    callback=lambda: len(multiprocessing.active_children()),
+    label='Process count',
+    description='Number of multiprocessing processes',
+)
+
+global_registry().gauge_callback(
+    name='python.multiprocessing.active',
+    callback=lambda: sum(1 for proc in multiprocessing.active_children() if proc.is_alive()),
+    label='Active multiprocessing processes',
+    description='Number of active multiprocessing processes',
+)
+
+global_registry().gauge_callback(
+    name='python.multiprocessing.daemon',
+    callback=lambda: sum(1 for proc in multiprocessing.active_children() if proc.daemon),
+    label='Daemon processes count',
+    description='Number of daemon multiprocessing processes',
+)
+
+# ------------------------------------------------------------------------------
+
+for i in xrange(3):
+  global_registry().gauge_callback(
+      name='python.gc.collection.count%s' % i,
+      callback=lambda: gc.get_count()[i],
+      label='GC collection count %s' % i,
+      description='Current collection counts',
+  )
+
+global_registry().gauge_callback(
+    name='python.gc.objects.count',
+    callback=lambda: len(gc.get_objects()),
+    label='GC tracked object count',
+    description='Number of objects being tracked by the garbage collector',
+)
+
+global_registry().gauge_callback(
+    name='python.gc.referrers.count',
+    callback=lambda: len(gc.get_referrers()),
+    label='GC tracked object referrers',
+    description='Number of objects that directly refer to any objects',
+)
+
+global_registry().gauge_callback(
+    name='python.gc.referents.count',
+    callback=lambda: len(gc.get_referrers()),
+    label='GC tracked object referents',
+    description='Number of objects that directly referred to any objects',
+)
+
+# ------------------------------------------------------------------------------
+
+active_requests = global_registry().counter(
+    name='desktop.requests.active.count',
+    label='Active requests',
+    description='Number of currently active requests',
+)
+
+request_exceptions = global_registry().counter(
+    name='desktop.requests.exceptions.count',
+    label='Request exceptions',
+    description='Number requests that resulted in an exception',
+)
+
+response_time = global_registry().timer(
+    name='desktop.requests.aggregate-response-time',
+    label='Request aggregate response time',
+    description='Time taken to respond to requests'
+)
+
+# ------------------------------------------------------------------------------
+
+user_count = global_registry().gauge(
+    name='desktop.users.count',
+    label='User count',
+    description='Total number of users',
+)
+
+# Initialize with the current user count.
+user_count.set_value(User.objects.all().count())
+
+@receiver(post_save, sender=User)
+def user_post_save_handler(sender, **kwargs):
+  if 'created' in kwargs:
+    user_count.set_value(User.objects.all().count())
+
+@receiver(post_delete, sender=User)
+def user_post_delete_handler(sender, **kwargs):
+  user_count.set_value(User.objects.all().count())
+
+logged_in_users = global_registry().counter(
+    name='desktop.users.logged-in.count',
+    label='Number of logged in users',
+    description='Number of logged in users',
+)
+
+@receiver(user_logged_in)
+def user_logged_in_handler(sender, **kwargs):
+  logged_in_users.inc()
+
+@receiver(user_logged_out)
+def user_logged_out_handler(sender, **kwargs):
+  logged_in_users.dec()

+ 20 - 0
desktop/core/src/desktop/middleware.py

@@ -49,6 +49,7 @@ from desktop.lib.exceptions import StructuredException
 from desktop.lib.exceptions_renderable import PopupException
 from desktop.log.access import access_log, log_page_hit
 from desktop import appmanager
+from desktop import metrics
 from hadoop import cluster
 from desktop.log import get_audit_logger
 
@@ -619,3 +620,22 @@ class EnsureSafeRedirectURLMiddleware(object):
       return response
     else:
       return response
+
+
+class MetricsMiddleware(object):
+  """
+  Middleware to track the number of active requests.
+  """
+
+  def process_request(self, request):
+    self._response_timer = metrics.response_time.time()
+    metrics.active_requests.inc()
+
+  def process_exception(self, request, exception):
+    self._response_timer.stop()
+    metrics.request_exceptions.inc()
+
+  def process_response(self, request, response):
+    self._response_timer.stop()
+    metrics.active_requests.dec()
+    return response

+ 1 - 0
desktop/core/src/desktop/settings.py

@@ -124,6 +124,7 @@ TEMPLATE_LOADERS = (
 
 MIDDLEWARE_CLASSES = [
     # The order matters
+    'desktop.middleware.MetricsMiddleware',
     'desktop.middleware.EnsureSafeMethodMiddleware',
     'desktop.middleware.AuditLoggingMiddleware',
     'django.middleware.common.CommonMiddleware',

+ 6 - 1
desktop/core/src/desktop/urls.py

@@ -34,6 +34,7 @@ from django.conf.urls.static import static
 from django.contrib import admin
 
 from desktop import appmanager
+from desktop import metrics
 
 # Django expects handler404 and handler500 to be defined.
 # django.conf.urls provides them. But we want to override them.
@@ -99,10 +100,14 @@ dynamic_patterns += patterns('useradmin.views',
   (r'^desktop/api/users/autocomplete', 'list_for_autocomplete'),
 )
 
+# Metrics specific
 dynamic_patterns += patterns('',
-  (r'^admin/', include(admin.site.urls)),
+  (r'^desktop/metrics/', include('desktop.lib.metrics.urls'))
 )
 
+dynamic_patterns += patterns('',
+  (r'^admin/', include(admin.site.urls)),
+)
 
 static_patterns = []