├── django_redis_monitor ├── __init__.py ├── mysql_backend │ ├── __init__.py │ └── base.py ├── sqlite3_backend │ ├── __init__.py │ └── base.py ├── postgresql_psycopg2_backend │ ├── __init__.py │ └── base.py ├── templates │ └── django_redis_monitor │ │ ├── nagios.xml │ │ ├── monitor_totals_only.html │ │ └── monitor.html ├── middleware.py ├── views.py ├── cursor_wrapper.py └── redis_monitor.py ├── redis_monitor_demo ├── __init__.py ├── .gitignore ├── urls.py ├── manage.py └── settings.py ├── LICENSE.txt └── README.txt /django_redis_monitor/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /redis_monitor_demo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_redis_monitor/mysql_backend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_redis_monitor/sqlite3_backend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /redis_monitor_demo/.gitignore: -------------------------------------------------------------------------------- 1 | data.db 2 | 3 | -------------------------------------------------------------------------------- /django_redis_monitor/postgresql_psycopg2_backend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_redis_monitor/templates/django_redis_monitor/nagios.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ db_count }} 5 | {{ db_total_ms }} 6 | 7 | 8 | {{ request_count }} 9 | {{ request_total_ms }} 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /redis_monitor_demo/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | from django.http import HttpResponse 3 | import time 4 | from random import random 5 | 6 | from django.contrib import admin 7 | admin.autodiscover() 8 | 9 | urlpatterns = patterns('', 10 | (r'^$', lambda request: time.sleep(random()) or HttpResponse('Hello!')), 11 | (r'^admin/(.*)', admin.site.root), 12 | ('^redis-monitor/$', 'django_redis_monitor.views.monitor'), 13 | ) 14 | -------------------------------------------------------------------------------- /redis_monitor_demo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | sys.path.append('../') 4 | 5 | from django.core.management import execute_manager 6 | try: 7 | import settings # Assumed to be in the same directory. 8 | except ImportError: 9 | import sys 10 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 11 | sys.exit(1) 12 | 13 | if __name__ == "__main__": 14 | execute_manager(settings) 15 | -------------------------------------------------------------------------------- /django_redis_monitor/mysql_backend/base.py: -------------------------------------------------------------------------------- 1 | from django.db.backends import * 2 | from django.db.backends.mysql.base import DatabaseClient 3 | from django.db.backends.mysql.base import DatabaseCreation 4 | from django.db.backends.mysql.base import DatabaseIntrospection 5 | from django.db.backends.mysql.base import DatabaseFeatures 6 | from django.db.backends.mysql.base import DatabaseOperations 7 | from django.db.backends.mysql.base import DatabaseWrapper \ 8 | as OriginalDatabaseWrapper 9 | 10 | from django_redis_monitor.cursor_wrapper import MonitoredCursorWrapper 11 | 12 | class DatabaseWrapper(OriginalDatabaseWrapper): 13 | 14 | def _cursor(self): 15 | cursor = super(DatabaseWrapper, self)._cursor() 16 | return MonitoredCursorWrapper(cursor, self) 17 | -------------------------------------------------------------------------------- /django_redis_monitor/sqlite3_backend/base.py: -------------------------------------------------------------------------------- 1 | from django.db.backends import * 2 | from django.db.backends.sqlite3.base import DatabaseClient 3 | from django.db.backends.sqlite3.base import DatabaseCreation 4 | from django.db.backends.sqlite3.base import DatabaseIntrospection 5 | from django.db.backends.sqlite3.base import DatabaseFeatures 6 | from django.db.backends.sqlite3.base import DatabaseOperations 7 | from django.db.backends.sqlite3.base import DatabaseWrapper \ 8 | as OriginalDatabaseWrapper 9 | 10 | from django_redis_monitor.cursor_wrapper import MonitoredCursorWrapper 11 | 12 | class DatabaseWrapper(OriginalDatabaseWrapper): 13 | 14 | def _cursor(self): 15 | cursor = super(DatabaseWrapper, self)._cursor() 16 | return MonitoredCursorWrapper(cursor, self) 17 | -------------------------------------------------------------------------------- /django_redis_monitor/postgresql_psycopg2_backend/base.py: -------------------------------------------------------------------------------- 1 | from django.db.backends import * 2 | from django.db.backends.postgresql_psycopg2.base import DatabaseClient 3 | from django.db.backends.postgresql_psycopg2.base import DatabaseCreation 4 | from django.db.backends.postgresql_psycopg2.base import DatabaseIntrospection 5 | from django.db.backends.postgresql_psycopg2.base import DatabaseFeatures 6 | from django.db.backends.postgresql_psycopg2.base import DatabaseOperations 7 | from django.db.backends.postgresql_psycopg2.base import DatabaseWrapper \ 8 | as OriginalDatabaseWrapper 9 | from django.db.backends.postgresql_psycopg2.base import get_version 10 | 11 | from django_redis_monitor.cursor_wrapper import MonitoredCursorWrapper 12 | 13 | class DatabaseWrapper(OriginalDatabaseWrapper): 14 | 15 | def _cursor(self): 16 | cursor = super(DatabaseWrapper, self)._cursor() 17 | return MonitoredCursorWrapper(cursor, self) 18 | -------------------------------------------------------------------------------- /django_redis_monitor/templates/django_redis_monitor/monitor_totals_only.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | Monitoring 6 | 23 | 24 | 25 |

Monitoring

26 | 27 |

Requests

28 | 29 |

Total requests since counting started: {{ requests.hits }}

30 |

Sum of request duration since counting started: {{ requests.weight }} microseconds (millionths of a second)

31 | 32 |

SQL queries

33 | 34 |

Total queries since counting started: {{ sqlops.hits }}

35 |

Sum of queries duration since counting started: {{ sqlops.weight }} microseconds (millionths of a second)

36 | 37 | 38 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Simon Willison 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /django_redis_monitor/middleware.py: -------------------------------------------------------------------------------- 1 | from django.db.backends import BaseDatabaseWrapper 2 | from django.conf import settings 3 | from redis_monitor import get_instance 4 | import time, logging 5 | 6 | class RedisMonitorMiddleware(object): 7 | def process_request(self, request): 8 | if self.should_track_request(request): 9 | self.tracking = True 10 | self.start_time = time.time() 11 | self.rm = get_instance('requests') 12 | else: 13 | self.tracking = False 14 | 15 | def process_response(self, request, response): 16 | if getattr(self, 'tracking', False): 17 | duration = time.time() - self.start_time 18 | duration_in_microseconds = int(1000000 * duration) 19 | try: 20 | self.rm.record_hit_with_weight(duration_in_microseconds) 21 | except Exception, e: 22 | logging.warn('RedisMonitor error: %s' % str(e)) 23 | return response 24 | 25 | def should_track_request(self, request): 26 | blacklist = getattr(settings, 'REDIS_MONITOR_REQUEST_BLACKLIST', []) 27 | for item in blacklist: 28 | if isinstance(item, basestring) and request.path == item: 29 | return False 30 | elif hasattr(item, 'match') and item.match(request.path): 31 | return False 32 | return True 33 | -------------------------------------------------------------------------------- /django_redis_monitor/templates/django_redis_monitor/monitor.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | Monitoring 6 | 23 | 24 | 25 |

Monitoring

26 | 27 |
28 |

Requests per second (for 10 second buckets):

29 | 30 | 31 | 32 | 33 | 34 | {% for obj in requests %} 35 | 36 | 37 | 38 | 39 | 40 | 41 | {% endfor %} 42 |
TimeReqs in 10 secsReqs/secondTotal req durationAvg duration per req
{{ obj.0|date:"H:i:s" }}TODO{{ obj.1|floatformat:"3" }}TODOTODO
43 |
44 | 45 |
46 |

SQLops per second (for 10 second buckets):

47 | 48 | 49 | 50 | 51 | {% for obj in sqlops %} 52 | 53 | 54 | 55 | 56 | {% endfor %} 57 |
TimeSQLops/second
{{ obj.0|date:"H:i:s" }}{{ obj.1|floatformat:"3" }}
58 |
59 | 60 | -------------------------------------------------------------------------------- /django_redis_monitor/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render_to_response as render 2 | from django.http import HttpResponse 3 | from django.conf import settings 4 | from redis_monitor import get_instance 5 | 6 | def monitor(request): 7 | requests = get_instance('requests') 8 | sqlops = get_instance('sqlops') 9 | if getattr(settings, 'REDIS_MONITOR_ONLY_TRACK_TOTALS', False): 10 | return render('django_redis_monitor/monitor_totals_only.html', { 11 | 'requests': requests.get_totals(), 12 | 'sqlops': sqlops.get_totals(), 13 | }) 14 | else: 15 | return render('django_redis_monitor/monitor.html', { 16 | 'requests': reversed( 17 | list(requests.get_recent_hits_per_second(minutes = 10)) 18 | ), 19 | 'sqlops': reversed( 20 | list(sqlops.get_recent_hits_per_second(minutes = 10)) 21 | ), 22 | }) 23 | 24 | def nagios(request): 25 | if not getattr(settings, 'REDIS_MONITOR_ONLY_TRACK_TOTALS', False): 26 | return HttpResponse( 27 | 'nagios only available in REDIS_MONITOR_ONLY_TRACK_TOTALS mode' 28 | ) 29 | requests = get_instance('requests').get_totals() 30 | sqlops = get_instance('sqlops').get_totals() 31 | return render('django_redis_monitor/nagios.xml', { 32 | 'db_count': sqlops.get('hits', 0), 33 | 'db_total_ms': int(int(sqlops.get('weight', 0)) / 1000.0), 34 | 'request_count': requests.get('hits', 0), 35 | 'request_total_ms': int(int(requests.get('weight', 0)) / 1000.0), 36 | }) 37 | -------------------------------------------------------------------------------- /django_redis_monitor/cursor_wrapper.py: -------------------------------------------------------------------------------- 1 | from redis_monitor import get_instance 2 | import time 3 | 4 | class MonitoredCursorWrapper(object): 5 | def __init__(self, cursor, db): 6 | self.cursor = cursor 7 | self.db = db 8 | self.rm = get_instance('sqlops') 9 | 10 | def execute(self, sql, params=()): 11 | start = time.time() 12 | try: 13 | return self.cursor.execute(sql, params) 14 | finally: 15 | stop = time.time() 16 | duration_in_microseconds = int(1000000 * (stop - start)) 17 | try: 18 | self.rm.record_hit_with_weight(duration_in_microseconds) 19 | except Exception, e: 20 | pass #logging.warn('RedisMonitor error: %s' % str(e)) 21 | 22 | def executemany(self, sql, param_list): 23 | start = time.time() 24 | try: 25 | return self.cursor.executemany(sql, param_list) 26 | finally: 27 | stop = time.time() 28 | duration_in_microseconds = int(1000000 * (stop - start)) 29 | try: 30 | self.rm.record_hits_with_total_weight( 31 | len(param_list), duration_in_microseconds 32 | ) 33 | except Exception, e: 34 | pass #logging.warn('RedisMonitor error: %s' % str(e)) 35 | 36 | def __getattr__(self, attr): 37 | if attr in self.__dict__: 38 | return self.__dict__[attr] 39 | else: 40 | return getattr(self.cursor, attr) 41 | 42 | def __iter__(self): 43 | return iter(self.cursor) 44 | -------------------------------------------------------------------------------- /redis_monitor_demo/settings.py: -------------------------------------------------------------------------------- 1 | DEBUG = True 2 | TEMPLATE_DEBUG = DEBUG 3 | 4 | ADMINS = () 5 | MANAGERS = ADMINS 6 | 7 | DATABASE_ENGINE = 'django_redis_monitor.sqlite3_backend' 8 | DATABASE_NAME = 'data.db' 9 | DATABASE_USER = '' 10 | DATABASE_PASSWORD = '' 11 | DATABASE_HOST = '' 12 | DATABASE_PORT = '' 13 | 14 | TIME_ZONE = 'UTC' 15 | 16 | LANGUAGE_CODE = 'en-us' 17 | SITE_ID = 1 18 | 19 | USE_I18N = True 20 | 21 | MEDIA_ROOT = '' 22 | MEDIA_URL = '' 23 | 24 | ADMIN_MEDIA_PREFIX = '/media/' 25 | 26 | SECRET_KEY = 'u3o25&^sgu79))-09v!ekid%1cbsa^h%75o3p%u_voh3&93vl1' 27 | 28 | TEMPLATE_LOADERS = ( 29 | 'django.template.loaders.filesystem.load_template_source', 30 | 'django.template.loaders.app_directories.load_template_source', 31 | ) 32 | 33 | MIDDLEWARE_CLASSES = ( 34 | 'django_redis_monitor.middleware.RedisMonitorMiddleware', 35 | 'django.middleware.common.CommonMiddleware', 36 | 'django.contrib.sessions.middleware.SessionMiddleware', 37 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 38 | ) 39 | 40 | ROOT_URLCONF = 'redis_monitor_demo.urls' 41 | 42 | TEMPLATE_DIRS = ( 43 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 44 | # Always use forward slashes, even on Windows. 45 | # Don't forget to use absolute paths, not relative paths. 46 | ) 47 | 48 | INSTALLED_APPS = ( 49 | 'django.contrib.auth', 50 | 'django.contrib.contenttypes', 51 | 'django.contrib.sessions', 52 | 'django.contrib.sites', 53 | 'django.contrib.flatpages', 54 | 'django.contrib.admin', 55 | 'django_redis_monitor', # For its templates directory 56 | ) 57 | 58 | import re 59 | REDIS_MONITOR_REQUEST_BLACKLIST = ( 60 | '/favicon.ico', 61 | '/redis-monitor/', 62 | re.compile('^/static/'), 63 | ) 64 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | django-redis-monitor 2 | ==================== 3 | 4 | Redis-backed performance monitoring for Django. Can keep track of Django 5 | requests per second, Django SQL operations per second and the average 6 | duration per request or per SQL operation over time. 7 | 8 | Numbers of hits and overall weights are stored in 10 second buckets, to save 9 | on space. Only results from previous complete buckets are returned, so it can 10 | take up to 10 seconds for a hit to be registered in the hits-per-second 11 | metrics. 12 | 13 | Installation: 14 | ------------- 15 | 16 | You will need to install and run a Redis server using a recent version of 17 | Redis (one that supports the HINCRBY and HGETALL commands). Redis 2.0.0 or 18 | higher should be fine. 19 | 20 | You will also need redis-py from http://github.com/andymccurdy/redis-py/ - 21 | you can pip install the latest version like so: 22 | 23 | pip install http://github.com/andymccurdy/redis-py/tarball/master 24 | 25 | Usage: 26 | ------ 27 | 28 | 1. Ensure django_redis_monitor is on your Python path. 29 | 30 | 2. Add RedisMonitorMiddleware to your middleware, at the top of the stack 31 | so that the performance overhead added by other middlewares is included 32 | in the request duration calculation: 33 | 34 | MIDDLEWARE_CLASSES = ( 35 | 'django_redis_monitor.middleware.RedisMonitorMiddleware', 36 | ) + MIDDLEWARE_CLASSES 37 | 38 | 3. Set your DATABASE_ENGINE setting to 'django_redis_monitor.sqlite3_backend' 39 | or 'django_redis_monitor.postgresql_psycopg2_backend' or 40 | 'django_redis_monitor.mysql_backend' so your SQL queries can be 41 | intercepted and counted. 42 | 43 | If you are using South for migrations, you will also need to add a 44 | corresponding SOUTH_DATABASE_ADAPTERS setting: 45 | 46 | SOUTH_DATABASE_ADAPTERS = { 47 | 'default': 'south.db.postgresql_psycopg2', 48 | # Or 'south.db.sqlite3' or 'south.db.mysql' 49 | } 50 | 51 | 4. Optional step: Add redis settings to your settings.py file (otherwise 52 | the following defaults will be used): 53 | 54 | REDIS_MONITOR_HOST = 'localhost' 55 | REDIS_MONITOR_PORT = 6379 56 | REDIS_MONITOR_DB = 0 57 | 58 | 5. Add 'django_redis_monitor' to your INSTALLED_APPS setting so Django can 59 | find the correct template for the monitor view. Alternatively, copy the 60 | monitor.html template in to a django_redis_monitor directory in your 61 | existing templates/ directory. 62 | 63 | 7. By default, django_redis_monitor records statistics over time in to 64 | separate buckets. If you are using an external monitoring tool such as 65 | Nagios you may only need to report the total number or duration of requests 66 | since counting began. If so, you can add the following setting: 67 | 68 | REDIS_MONITOR_ONLY_TRACK_TOTALS = True 69 | 70 | 8. Hook up the monitoring view function in your urls.py: 71 | 72 | urlpatterns = patterns('', 73 | # ... 74 | (r'^redis-monitor/$', 'django_redis_monitor.views.monitor'), 75 | ) 76 | 77 | If you plan to use the nagios reporting hook, add this as well: 78 | 79 | (r'^redis-monitor/nagios\.xml$', 'django_redis_monitor.views.nagios'), 80 | 81 | If you want the monitoring view to only be visible to super users, do this: 82 | 83 | from django_redis_monitor.views import monitor 84 | from django.contrib.auth.decorators import user_passes_test 85 | 86 | def requires_superuser(view_fn): 87 | decorator = user_passes_test(lambda u: u.is_superuser) 88 | return decorator(view_fn) 89 | 90 | urlpatterns = patterns('', 91 | # ... 92 | ('^redis-monitor/$', requires_superuser(monitor)), 93 | ) 94 | 95 | 9. Hit your application with a bunch of requests. 96 | 97 | 10. Go to http://localhost:8000/redis-monitor/ to see the results. 98 | 99 | 11. If you want to ignore requests to certain URLs (the redis-monitor view 100 | for example) you can add an optional REDIS_MONITOR_REQUEST_BLACKLIST 101 | setting. This can contain either strings or regular expressions: 102 | 103 | import re 104 | REDIS_MONITOR_REQUEST_BLACKLIST = ( 105 | '/favicon.ico', 106 | '/redis-monitor/nagios.xml', 107 | re.compile('^/static/'), 108 | ) 109 | -------------------------------------------------------------------------------- /django_redis_monitor/redis_monitor.py: -------------------------------------------------------------------------------- 1 | import datetime # we use utcnow to insulate against daylight savings errors 2 | import redis 3 | 4 | class RedisMonitor(object): 5 | def __init__(self, prefix='', redis_obj=None, redis_host='localhost', 6 | redis_port=6379, redis_db=0 7 | ): 8 | assert prefix and ' ' not in prefix, \ 9 | 'prefix (e.g. "rps") is required and must not contain spaces' 10 | self.prefix = prefix 11 | if redis_obj is None: 12 | redis_obj = redis.Redis( 13 | host=redis_host, port=redis_port, db=redis_db 14 | ) 15 | self.r = redis_obj 16 | 17 | def _hash_and_slot(self, dt = None): 18 | dt = dt or datetime.datetime.utcnow() 19 | hash = dt.strftime('%Y%m%d:%H') # 20100709:12 = 12th hour of that day 20 | slot = '%02d:%d' % ( # 24:3 for seconds 30-39 in minute 24 21 | dt.minute, dt.second / 10 22 | ) 23 | return ('%s:%s' % (self.prefix, hash), slot) 24 | 25 | def _calculate_start(self, hours, minutes, seconds, now = None): 26 | now = now or datetime.datetime.utcnow() 27 | delta = (60 * 60 * hours) + (60 * minutes) + seconds 28 | return now - datetime.timedelta(seconds = delta) 29 | 30 | def record_hit(self): 31 | self.record_hits(1) 32 | 33 | def record_hits(self, num_hits): 34 | hash, slot = self._hash_and_slot() 35 | self.r.hincrby(hash, slot, num_hits) 36 | 37 | def record_hit_with_weight(self, weight): 38 | self.record_hits_with_total_weight(1, weight) 39 | 40 | def record_hits_with_total_weight(self, num_hits, total_weight): 41 | hash, slot = self._hash_and_slot() 42 | self.r.hincrby(hash, slot, num_hits) 43 | self.r.hincrby(hash, slot + 'w', total_weight) 44 | 45 | def get_recent_hits(self, hours = 0, minutes = 0, seconds = 0): 46 | gathered = self.get_recent_hits_and_weights(hours, minutes, seconds) 47 | for date, hits, weight in gathered: 48 | yield date, hits 49 | 50 | def get_recent_hits_and_weights( 51 | self, hours = 0, minutes = 0, seconds = 0 52 | ): 53 | start = self._calculate_start(hours, minutes, seconds) 54 | start = start.replace( 55 | second = (start.second / 10) * 10, microsecond = 0 56 | ) 57 | preloaded_hashes = {} 58 | gathered = [] 59 | current = start 60 | now = datetime.datetime.utcnow().replace( 61 | second = (start.second / 10) * 10, microsecond = 0 62 | ) 63 | while current < now: 64 | hash, slot = self._hash_and_slot(current) 65 | if hash not in preloaded_hashes: 66 | preloaded_hashes[hash] = self.r.hgetall(hash) 67 | hits = int(preloaded_hashes[hash].get(slot, 0)) 68 | weight = int(preloaded_hashes[hash].get(slot + 'w', 0)) 69 | gathered.append((current, hits, weight)) 70 | current += datetime.timedelta(seconds = 10) 71 | return gathered 72 | 73 | def get_recent_hits_per_second(self, hours = 0, minutes = 0, seconds = 0): 74 | gathered = self.get_recent_hits(hours, minutes, seconds) 75 | for date, hits in gathered: 76 | yield date, hits / 10.0 77 | 78 | def get_recent_avg_weights(self, hours = 0, minutes = 0, seconds = 0): 79 | gathered = self.get_recent_hits_and_weights(hours, minutes, seconds) 80 | for date, hits, weight in gathered: 81 | if weight == 0 or hits == 0: 82 | yield date, 0 83 | else: 84 | yield date, float(weight) / hits 85 | 86 | class RedisMonitorTotalsOnly(RedisMonitor): 87 | 88 | def record_hits_with_total_weight(self, num_hits, total_weight): 89 | hash = '%s:totals' % self.prefix 90 | self.r.hincrby(hash, 'hits', num_hits) 91 | self.r.hincrby(hash, 'weight', total_weight) 92 | 93 | def get_recent_hits_and_weights(self, *args, **kwargs): 94 | raise NotImplemented, 'REDIS_MONITOR_ONLY_TRACK_TOTALS mode' 95 | 96 | def get_totals(self): 97 | hash = '%s:totals' % self.prefix 98 | return self.r.hgetall(hash) or {} 99 | 100 | def get_instance(prefix): 101 | from django.conf import settings 102 | from django.core import signals 103 | host = getattr(settings, 'REDIS_MONITOR_HOST', 'localhost') 104 | port = getattr(settings, 'REDIS_MONITOR_PORT', 6379) 105 | db = getattr(settings, 'REDIS_MONITOR_DB', 0) 106 | only_track_totals = getattr( 107 | settings, 'REDIS_MONITOR_ONLY_TRACK_TOTALS', False 108 | ) 109 | if only_track_totals: 110 | klass = RedisMonitorTotalsOnly 111 | else: 112 | klass = RedisMonitor 113 | obj = klass(prefix, redis_host=host, redis_port=port, redis_db=db) 114 | # Ensure we disconnect at the end of the request cycle 115 | signals.request_finished.connect( 116 | lambda **kwargs: obj.r.connection.disconnect() 117 | ) 118 | return obj 119 | 120 | --------------------------------------------------------------------------------