├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── examples ├── __init__.py ├── demo │ ├── __init__.py │ ├── models.py │ ├── templates │ │ └── demo │ │ │ └── index.html │ ├── tests.py │ ├── urls.py │ └── views.py ├── manage.py ├── settings.py └── urls.py ├── memcache_toolbar ├── __init__.py ├── panels │ ├── __init__.py │ ├── memcache.py │ └── pylibmc.py └── templates │ └── memcache_toolbar │ └── panels │ └── memcache.html ├── requirements.txt ├── setup.py └── test └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | env/ 3 | *.pyc 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Ross McFarland and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Django nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include memcache_toolbar/templates * -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | Memcache Debug Toolbar 3 | ====================== 4 | 5 | The Memcache Debug Toolbar is an add-on for Django Debug Toolbar for tracking 6 | memcached usage. It currently supports both the ``pylibmc`` and ``memcache`` libraries. 7 | 8 | This is definitely beta software, but I've found it useful in work and personal 9 | projects. Feedback welcome, patches appreciated. - Ross McFarland 10 | 11 | Installation 12 | ============ 13 | 14 | #. Install and configure `Django Debug Toolbar `_. 15 | 16 | #. Add the ``memcache_toolbar`` app to your ``INSTALLED_APPS``. 17 | 18 | #. Import the panel corresponding to the library you're using. 19 | 20 | The following must be imported in your ``settings.py`` file so that it has a 21 | chance to replace the caching library with one capable of tracking. You'll 22 | probably want to import it in ``local_settings.py`` (if you use the pattern) or 23 | at least wrap the import line in ``if DEBUG``: 24 | 25 | For ``memcache``:: 26 | 27 | import memcache_toolbar.panels.memcache 28 | 29 | For ``pylibmc``:: 30 | 31 | import memcache_toolbar.panels.pylibmc 32 | 33 | Configuration 34 | ============= 35 | 36 | #. Add the ``memcache`` or ``pylibmc`` panel to ``DEBUG_TOOLBAR_PANELS``. 37 | 38 | You'll need to add the panel corresponding to the library you'll be using to 39 | the list of debug toolbar's panels in the order in which you'd like it to 40 | appear:: 41 | 42 | DEBUG_TOOLBAR_PANELS = ( 43 | ... 44 | 'memcache_toolbar.panels.memcache.MemcachePanel', 45 | # if you use pylibmc you'd include its panel instead 46 | # 'memcache_toolbar.panels.pylibmc.PylibmcPanel', 47 | ) 48 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ross/memcache-debug-panel/7ae81e7f77e39ecf58cac1c5d98cdf3ec235f3cc/examples/__init__.py -------------------------------------------------------------------------------- /examples/demo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ross/memcache-debug-panel/7ae81e7f77e39ecf58cac1c5d98cdf3ec235f3cc/examples/demo/__init__.py -------------------------------------------------------------------------------- /examples/demo/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /examples/demo/templates/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | memcache 4 | 5 | 6 |

memcache

7 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/demo/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates two different styles of tests (one doctest and one 3 | unittest). These will both pass when you run "manage.py test". 4 | 5 | Replace these with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | 11 | class SimpleTest(TestCase): 12 | def test_basic_addition(self): 13 | """ 14 | Tests that 1 + 1 always equals 2. 15 | """ 16 | self.failUnlessEqual(1 + 1, 2) 17 | 18 | __test__ = {"doctest": """ 19 | Another way to test that 1 + 1 is equal to 2. 20 | 21 | >>> 1 + 1 == 2 22 | True 23 | """} 24 | -------------------------------------------------------------------------------- /examples/demo/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | urlpatterns = patterns( 4 | '', 5 | url(r'^$', 'demo.views.index', {}, 'index'), 6 | url(r'^cached$', 'demo.views.cached', {}, 'cached'), 7 | ) 8 | -------------------------------------------------------------------------------- /examples/demo/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render_to_response 2 | from django.views.decorators.cache import cache_page 3 | from django.core.cache import cache 4 | 5 | 6 | def index(request, **kwargs): 7 | cache.get('key') 8 | cache.get('key2') 9 | try: 10 | cache.incr('hello') 11 | except: 12 | pass 13 | return render_to_response('demo/index.html') 14 | 15 | 16 | @cache_page(60 * 15, key_prefix='demo') 17 | def cached(request, **kwargs): 18 | return index(request, **kwargs) 19 | -------------------------------------------------------------------------------- /examples/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | try: 4 | import settings # Assumed to be in the same directory. 5 | except ImportError: 6 | import sys 7 | 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__) 8 | sys.exit(1) 9 | 10 | if __name__ == "__main__": 11 | execute_manager(settings) 12 | -------------------------------------------------------------------------------- /examples/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for examples project. 2 | 3 | import sys 4 | from os.path import dirname, join 5 | import logging 6 | logging.basicConfig(level=logging.DEBUG) 7 | 8 | # add our parent directory to the path so that we can find memcache_toolbar 9 | sys.path.append('../') 10 | 11 | # in order to track django's caching we need to import the panels code now 12 | # so that it can swap out the client with one that tracks usage. 13 | import memcache_toolbar.panels.memcache 14 | # if you're using pylibmc use the following instead 15 | #import memcache_toolbar.panels.pylibmc 16 | 17 | DEBUG = True 18 | TEMPLATE_DEBUG = DEBUG 19 | 20 | DATABASES = { 21 | 'default': { 22 | 'ENGINE': 'django.db.backends.sqlite3', 23 | 'NAME': 'examples.sqlite3', 24 | 'USER': '', 25 | 'PASSWORD': '', 26 | 'HOST': '', 27 | 'PORT': '', 28 | } 29 | } 30 | 31 | CACHE_BACKEND = 'memcached://127.0.0.1:11211/' 32 | 33 | TIME_ZONE = 'UTC' 34 | LANGUAGE_CODE = 'en-us' 35 | SITE_ID = 1 36 | USE_I18N = True 37 | USE_L10N = True 38 | SECRET_KEY = 'rfca2x2s3465+3+=-6m!(!f3%nvy^d@g0_ykgawt*%6exoe3ti' 39 | 40 | TEMPLATE_LOADERS = ( 41 | 'django.template.loaders.filesystem.Loader', 42 | 'django.template.loaders.app_directories.Loader', 43 | ) 44 | 45 | MIDDLEWARE_CLASSES = ( 46 | 'django.middleware.common.CommonMiddleware', 47 | 'debug_toolbar.middleware.DebugToolbarMiddleware', 48 | ) 49 | 50 | ROOT_URLCONF = 'examples.urls' 51 | 52 | TEMPLATE_DIRS = ( 53 | join(dirname(__file__), 'templates') 54 | ) 55 | 56 | INSTALLED_APPS = ( 57 | 'django.contrib.contenttypes', 58 | # external app 59 | 'debug_toolbar', 60 | 'memcache_toolbar', 61 | # apps 62 | 'demo', 63 | ) 64 | 65 | DEBUG_TOOLBAR_PANELS = ( 66 | 'debug_toolbar.panels.version.VersionDebugPanel', 67 | 'debug_toolbar.panels.timer.TimerDebugPanel', 68 | 'debug_toolbar.panels.settings_vars.SettingsVarsDebugPanel', 69 | 'debug_toolbar.panels.headers.HeaderDebugPanel', 70 | 'debug_toolbar.panels.request_vars.RequestVarsDebugPanel', 71 | 'debug_toolbar.panels.template.TemplateDebugPanel', 72 | 'debug_toolbar.panels.sql.SQLDebugPanel', 73 | 'debug_toolbar.panels.signals.SignalDebugPanel', 74 | 'debug_toolbar.panels.logger.LoggingPanel', 75 | 'memcache_toolbar.panels.memcache.MemcachePanel', 76 | # if you use pyibmc you'd include it's panel instead 77 | #'memcache_toolbar.panels.pylibmc.PylibmcPanel', 78 | ) 79 | 80 | DEBUG_TOOLBAR_CONFIG = { 81 | 'SHOW_TOOLBAR_CALLBACK': lambda request: True 82 | } 83 | -------------------------------------------------------------------------------- /examples/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | urlpatterns = patterns( 4 | '', 5 | url(r'^', include('examples.demo.urls')), 6 | ) 7 | -------------------------------------------------------------------------------- /memcache_toolbar/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ross/memcache-debug-panel/7ae81e7f77e39ecf58cac1c5d98cdf3ec235f3cc/memcache_toolbar/__init__.py -------------------------------------------------------------------------------- /memcache_toolbar/panels/__init__.py: -------------------------------------------------------------------------------- 1 | # work around modules with the same name 2 | from __future__ import absolute_import 3 | 4 | from datetime import datetime 5 | from debug_toolbar.panels import Panel 6 | from django.conf import settings 7 | from django.template.loader import render_to_string 8 | from django.utils.translation import ugettext_lazy as _ 9 | from os.path import dirname, realpath 10 | import django 11 | import logging 12 | import SocketServer 13 | import traceback 14 | 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | class Calls: 20 | def __init__(self): 21 | self.reset() 22 | 23 | def reset(self): 24 | self._calls = [] 25 | 26 | def append(self, call): 27 | self._calls.append(call) 28 | 29 | def calls(self): 30 | return self._calls 31 | 32 | def size(self): 33 | return len(self._calls) 34 | 35 | def last(self): 36 | return self._calls[-1] 37 | 38 | # NOTE this is not even close to thread-safe/aware 39 | instance = Calls() 40 | 41 | # based on the function with the same name in ddt's sql, i'd rather just use it 42 | # than copy it, but i can't import it without things blowing up 43 | django_path = realpath(dirname(django.__file__)) 44 | socketserver_path = realpath(dirname(SocketServer.__file__)) 45 | 46 | 47 | def tidy_stacktrace(strace): 48 | trace = [] 49 | for s in strace[:-1]: 50 | s_path = realpath(s[0]) 51 | if getattr(settings, 'DEBUG_TOOLBAR_CONFIG', {}).get( 52 | 'HIDE_DJANGO_SQL', True) \ 53 | and django_path in s_path and not 'django/contrib' in s_path: 54 | continue 55 | if socketserver_path in s_path: 56 | continue 57 | trace.append((s[0], s[1], s[2], s[3])) 58 | return trace 59 | 60 | 61 | def record(func): 62 | def recorder(*args, **kwargs): 63 | if getattr(settings, 'DEBUG_TOOLBAR_CONFIG', {}).get( 64 | 'ENABLE_STACKTRACES', True): 65 | stacktrace = tidy_stacktrace(traceback.extract_stack()) 66 | else: 67 | stacktrace = [] 68 | call = {'function': func.__name__, 'args': None, 69 | 'stacktrace': stacktrace} 70 | # the try here is just being extra safe, it should not happen 71 | try: 72 | a = None 73 | # first arg is self, do we have another 74 | if len(args) > 1: 75 | a = args[1] 76 | # is it a dictionary (most likely multi) 77 | if isinstance(a, dict): 78 | # just use it's keys 79 | a = a.keys() 80 | # store the args 81 | call['args'] = a 82 | except e: 83 | logger.exception('tracking of call args failed') 84 | ret = None 85 | try: 86 | # the clock starts now 87 | call['start'] = datetime.now() 88 | ret = func(*args, **kwargs) 89 | finally: 90 | # the clock stops now 91 | dur = datetime.now() - call['start'] 92 | call['duration'] = (dur.seconds * 1000) + (dur.microseconds / 1000.0) 93 | instance.append(call) 94 | return ret 95 | return recorder 96 | 97 | 98 | class BasePanel(Panel): 99 | name = 'Memcache' 100 | has_content = True 101 | 102 | def process_request(self, request): 103 | instance.reset() 104 | 105 | @property 106 | def nav_title(self): 107 | return _('Memcache') 108 | 109 | @property 110 | def nav_subtitle(self): 111 | duration = 0 112 | calls = instance.calls() 113 | for call in calls: 114 | duration += call['duration'] 115 | n = len(calls) 116 | if (n > 0): 117 | return "%d calls, %0.2fms" % (n, duration) 118 | else: 119 | return "0 calls" 120 | 121 | @property 122 | def title(self): 123 | return _('Memcache Calls') 124 | 125 | @property 126 | def url(self): 127 | return '' 128 | 129 | @property 130 | def template(self): 131 | return 'memcache_toolbar/panels/memcache.html' 132 | 133 | @property 134 | def content(self): 135 | duration = 0 136 | calls = instance.calls() 137 | for call in calls: 138 | duration += call['duration'] 139 | 140 | context = { 141 | 'calls': calls, 142 | 'count': len(calls), 143 | 'duration': duration, 144 | } 145 | 146 | return render_to_string(self.template, context) 147 | -------------------------------------------------------------------------------- /memcache_toolbar/panels/memcache.py: -------------------------------------------------------------------------------- 1 | # work around modules with the same name 2 | from __future__ import absolute_import 3 | 4 | from memcache_toolbar.panels import BasePanel, record 5 | import logging 6 | 7 | DEBUG = False 8 | 9 | logger = logging.getLogger(__name__) 10 | try: 11 | import memcache as memc 12 | 13 | origClient = None 14 | 15 | class TrackingMemcacheClient(memc.Client): 16 | 17 | @record 18 | def flush_all(self, *args, **kwargs): 19 | return origClient.flush_all(self, *args, **kwargs) 20 | 21 | @record 22 | def delete_multi(self, *args, **kwargs): 23 | return origClient.delete_multi(self, *args, **kwargs) 24 | 25 | @record 26 | def delete(self, *args, **kwargs): 27 | return origClient.delete(self, *args, **kwargs) 28 | 29 | @record 30 | def incr(self, *args, **kwargs): 31 | return origClient.incr(self, *args, **kwargs) 32 | 33 | @record 34 | def decr(self, *args, **kwargs): 35 | return origClient.decr(self, *args, **kwargs) 36 | 37 | @record 38 | def add(self, *args, **kwargs): 39 | return origClient.add(self, *args, **kwargs) 40 | 41 | @record 42 | def append(self, *args, **kwargs): 43 | return origClient.append(self, *args, **kwargs) 44 | 45 | @record 46 | def prepend(self, *args, **kwargs): 47 | return origClient.prepend(self, *args, **kwargs) 48 | 49 | @record 50 | def replace(self, *args, **kwargs): 51 | return origClient.replace(self, *args, **kwargs) 52 | 53 | @record 54 | def set(self, *args, **kwargs): 55 | return origClient.set(self, *args, **kwargs) 56 | 57 | @record 58 | def cas(self, *args, **kwargs): 59 | return origClient.cas(self, *args, **kwargs) 60 | 61 | @record 62 | def set_multi(self, *args, **kwargs): 63 | return origClient.set_multi(self, *args, **kwargs) 64 | 65 | @record 66 | def get(self, *args, **kwargs): 67 | return origClient.get(self, *args, **kwargs) 68 | 69 | @record 70 | def gets(self, *args, **kwargs): 71 | return origClient.gets(self, *args, **kwargs) 72 | 73 | @record 74 | def get_multi(self, *args, **kwargs): 75 | return origClient.get_multi(self, *args, **kwargs) 76 | 77 | # NOTE issubclass is true of both are the same class 78 | if not issubclass(memc.Client, TrackingMemcacheClient): 79 | logger.debug('installing memcache.Client with tracking') 80 | origClient = memc.Client 81 | memc.Client = TrackingMemcacheClient 82 | 83 | except: 84 | if DEBUG: 85 | logger.exception('unable to install memcache.Client with tracking') 86 | else: 87 | logger.debug('unable to install memcache.Client with tracking') 88 | 89 | 90 | class MemcachePanel(BasePanel): 91 | pass 92 | -------------------------------------------------------------------------------- /memcache_toolbar/panels/pylibmc.py: -------------------------------------------------------------------------------- 1 | # work around modules with the same name 2 | from __future__ import absolute_import 3 | 4 | from memcache_toolbar.panels import BasePanel, record 5 | import logging 6 | 7 | DEBUG = False 8 | 9 | logger = logging.getLogger(__name__) 10 | try: 11 | import pylibmc 12 | import _pylibmc 13 | 14 | # duplicating the client code sucks so hard, but it's necessary since the 15 | # original uses super and we'd need to inherit from it and then replace it 16 | # resulting in a class that inherits from itself :( see 17 | # http://fuhm.net/super-harmful/ anyway, see the bottom of this class for 18 | # the methods we're installing tracking on 19 | class TrackingPylibmcClient(_pylibmc.client): 20 | def __init__(self, servers, binary=False): 21 | """Initialize a memcached client instance. 22 | 23 | This connects to the servers in *servers*, which will default to being 24 | TCP servers. If it looks like a filesystem path, a UNIX socket. If 25 | prefixed with `udp:`, a UDP connection. 26 | 27 | If *binary* is True, the binary memcached protocol is used. 28 | """ 29 | self.binary = binary 30 | self.addresses = list(servers) 31 | addr_tups = [] 32 | for server in servers: 33 | addr = server 34 | port = 11211 35 | if server.startswith("udp:"): 36 | stype = _pylibmc.server_type_udp 37 | addr = addr[4:] 38 | if ":" in server: 39 | (addr, port) = addr.split(":", 1) 40 | port = int(port) 41 | elif ":" in server: 42 | stype = _pylibmc.server_type_tcp 43 | (addr, port) = server.split(":", 1) 44 | port = int(port) 45 | elif "/" in server: 46 | stype = _pylibmc.server_type_unix 47 | port = 0 48 | else: 49 | stype = _pylibmc.server_type_tcp 50 | addr_tups.append((stype, addr, port)) 51 | _pylibmc.client.__init__(self, servers=addr_tups, binary=binary) 52 | 53 | def __repr__(self): 54 | return "%s(%r, binary=%r)" % (self.__class__.__name__, 55 | self.addresses, self.binary) 56 | 57 | def __str__(self): 58 | addrs = ", ".join(map(str, self.addresses)) 59 | return "<%s for %s, binary=%r>" % (self.__class__.__name__, 60 | addrs, self.binary) 61 | 62 | def get_behaviors(self): 63 | """Gets the behaviors from the underlying C client instance. 64 | 65 | Reverses the integer constants for `hash` and `distribution` into more 66 | understandable string values. See *set_behaviors* for info. 67 | """ 68 | bvrs = _pylibmc.client.get_behaviors(self) 69 | bvrs["hash"] = hashers_rvs[bvrs["hash"]] 70 | bvrs["distribution"] = distributions_rvs[bvrs["distribution"]] 71 | return BehaviorDict(self, bvrs) 72 | 73 | def set_behaviors(self, behaviors): 74 | """Sets the behaviors on the underlying C client instance. 75 | 76 | Takes care of morphing the `hash` key, if specified, into the 77 | corresponding integer constant (which the C client expects.) If, 78 | however, an unknown value is specified, it's passed on to the C client 79 | (where it most surely will error out.) 80 | 81 | This also happens for `distribution`. 82 | """ 83 | behaviors = behaviors.copy() 84 | if behaviors.get("hash") is not None: 85 | behaviors["hash"] = hashers[behaviors["hash"]] 86 | if behaviors.get("ketama_hash") is not None: 87 | behaviors["ketama_hash"] = hashers[behaviors["ketama_hash"]] 88 | if behaviors.get("distribution") is not None: 89 | behaviors["distribution"] = distributions[behaviors["distribution"]] 90 | return _pylibmc.client.set_behaviors(self, behaviors) 91 | 92 | behaviors = property(get_behaviors, set_behaviors) 93 | 94 | @property 95 | def behaviours(self): 96 | raise AttributeError("nobody uses british spellings") 97 | 98 | # methods we're adding tracking to 99 | 100 | @record 101 | def get(self, *args, **kwargs): 102 | return _pylibmc.client.get(self, *args, **kwargs) 103 | 104 | @record 105 | def get_multi(self, *args, **kwargs): 106 | return _pylibmc.client.get_multi(self, *args, **kwargs) 107 | 108 | @record 109 | def set(self, *args, **kwargs): 110 | return _pylibmc.client.set(self, *args, **kwargs) 111 | 112 | @record 113 | def set_multi(self, *args, **kwargs): 114 | return _pylibmc.client.set_multi(self, *args, **kwargs) 115 | 116 | @record 117 | def add(self, *args, **kwargs): 118 | return _pylibmc.client.add(self, *args, **kwargs) 119 | 120 | @record 121 | def replace(self, *args, **kwargs): 122 | return _pylibmc.client.replace(self, *args, **kwargs) 123 | 124 | @record 125 | def append(self, *args, **kwargs): 126 | return _pylibmc.client.append(self, *args, **kwargs) 127 | 128 | @record 129 | def prepend(self, *args, **kwargs): 130 | return _pylibmc.client.prepend(self, *args, **kwargs) 131 | 132 | @record 133 | def incr(self, *args, **kwargs): 134 | return _pylibmc.client.incr(self, *args, **kwargs) 135 | 136 | @record 137 | def decr(self, *args, **kwargs): 138 | return _pylibmc.client.decr(self, *args, **kwargs) 139 | 140 | @record 141 | def delete(self, *args, **kwargs): 142 | return _pylibmc.client.delete(self, *args, **kwargs) 143 | 144 | # NOTE delete_multi is implemented by iterative over args calling delete 145 | # for each one. i could probably hide that here, but i actually think 146 | # it's best to show it since each one will be a seperate network 147 | # round-trip. 148 | @record 149 | def delete_multi(self, *args, **kwargs): 150 | return _pylibmc.client.delete_multi(self, *args, **kwargs) 151 | 152 | @record 153 | def flush_all(self, *args, **kwargs): 154 | return _pylibmc.client.flush_all(self, *args, **kwargs) 155 | 156 | # NOTE issubclass is true of both are the same class 157 | if not issubclass(pylibmc.Client, TrackingPylibmcClient): 158 | logger.debug('installing pylibmc.Client with tracking') 159 | pylibmc.Client = TrackingPylibmcClient 160 | 161 | except: 162 | if DEBUG: 163 | logger.exception('unable to install pylibmc.Client with tracking') 164 | else: 165 | logger.debug('unable to install pylibmc.Client with tracking') 166 | 167 | 168 | class PylibmcPanel(BasePanel): 169 | pass 170 | -------------------------------------------------------------------------------- /memcache_toolbar/templates/memcache_toolbar/panels/memcache.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |

{% trans 'Calls:' %}

3 |
{% blocktrans with count as count and duration as duration %}{{ count }} calls in {{ duration }}ms{% endblocktrans %}
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% for call in calls %} 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 57 | 58 | {% endfor %} 59 | 60 |
{% trans 'Start Time' %}{% trans 'Duration (ms)' %}{% trans 'Call' %}{% trans 'Args' %}{% trans 'Actions' %}
{{ call.start|date:'H:i:s.u' }}{{ call.duration }}{{ call.function }}{{ call.args }} 22 | {% trans 'Stacktrace' %} 23 |
61 | 75 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django 2 | django-debug-toolbar 3 | pylibmc 4 | python-memcached 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | setup( 6 | name='memcache_toolbar', 7 | version='0.5.5', 8 | description='Add-on for Django Debug Toolbar for tracking memcached usage', 9 | author='Ross McFarland', 10 | author_email='rwmcfa1@neces.com', 11 | url='http://github.com/ross/memcache-debug-panel', 12 | packages=find_packages(exclude=('examples', 'examples.demo', 'test')), 13 | provides=['memcache_toolbar'], 14 | requires=['Django', 'debug_toolbar'], 15 | include_package_data=True, 16 | zip_safe=False, 17 | ) 18 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import logging 4 | logging.basicConfig(level=logging.DEBUG) 5 | 6 | import unittest 7 | import pylibmc 8 | import memcache 9 | import memcache_toolbar.panels.memcache 10 | import memcache_toolbar.panels.pylibmc 11 | 12 | 13 | class TestCase(unittest.TestCase): 14 | 15 | def assertCall(self, call, expected_function, expected_args, base_message): 16 | self.assertTrue(call, base_message + ', no last call') 17 | self.assertEquals(call['function'], expected_function, 18 | base_message + 19 | ', last call function mis-match: %s != %s' % 20 | (expected_function, call['function'])) 21 | self.assertEquals(call['args'], expected_args, base_message + 22 | ', args mis-match: %s != %s' 23 | % (expected_args, call['args'])) 24 | self.assertTrue('start' in call, base_message + ', start missing') 25 | self.assertTrue('duration' in call, base_message + 26 | ', duration missing') 27 | 28 | def assertLastCall(self, expected_function, expected_args, base_message): 29 | self.assertCall(memcache_toolbar.panels.instance.last(), 30 | expected_function, expected_args, base_message) 31 | 32 | def assertLastCallRaised(self, expected_function, expected_args, 33 | base_message): 34 | call = memcache_toolbar.panels.instance.last() 35 | self.assertCall(call, expected_function, expected_args, base_message) 36 | self.assertTrue('exception' in call, base_message + 37 | ', did not throw an exception') 38 | 39 | 40 | class TestPyLibMc(TestCase): 41 | 42 | def test_basic(self): 43 | memcache_toolbar.panels.instance.reset() 44 | 45 | client = pylibmc.Client(['127.0.0.1'], binary=True) 46 | client.behaviors = {'tcp_nodelay': True, 'ketama': True} 47 | 48 | # flush_all, first so we're in a clean state 49 | self.assertTrue(client.flush_all(), 'flush_all') 50 | self.assertLastCall('flush_all', None, 'initial flush') 51 | 52 | # set 53 | key = 'key' 54 | value = 'value' 55 | self.assertTrue(client.set(key, value), 'simple set') 56 | self.assertLastCall('set', key, 'simple set') 57 | # get 58 | self.assertEqual(client.get(key), value, 'simple get') 59 | self.assertLastCall('get', key, 'simple get') 60 | # set_multi 61 | multi = {'key1': 'value1', 'array': ['a1', 'a2'], 'bool': True} 62 | multi_keys = multi.keys() 63 | self.assertEqual(client.set_multi(multi), [], 'set multi') 64 | self.assertLastCall('set_multi', multi_keys, 'set_multi') 65 | # get_multi 66 | self.assertEqual(client.get_multi(multi_keys), multi, 'get_multi') 67 | self.assertLastCall('get_multi', multi_keys, 'get_multi') 68 | # add 69 | add = 'add' 70 | self.assertTrue(client.add(add, value), 'simple add, success') 71 | self.assertLastCall('add', add, 'simple add, success') 72 | self.assertFalse(client.add(add, value), 'simple add, exists') 73 | self.assertLastCall('add', add, 'simple add, exists') 74 | # replace 75 | self.assertTrue(client.replace(add, value), 'simple replace, exists') 76 | self.assertLastCall('replace', add, 'simple replace, exists') 77 | non_existent = 'non-existent' 78 | self.assertRaises(pylibmc.NotFound, client.replace, non_existent, 79 | value) # 'simple replace, non-existent raises' 80 | self.assertLastCallRaised('replace', non_existent, 81 | 'simple replace, non-existent') 82 | # append 83 | self.assertTrue(client.append(key, value), 'simple append') 84 | self.assertLastCall('append', key, 'simple append, exists') 85 | empty = 'empty' 86 | self.assertFalse(client.append(empty, value), 'simple append, empty') 87 | self.assertLastCall('append', empty, 'simple append, empty') 88 | # prepend 89 | self.assertTrue(client.prepend(key, value), 'simple prepend') 90 | self.assertLastCall('prepend', key, 'simmple prepend, exists') 91 | self.assertFalse(client.prepend(empty, value), 'simple prepend, empty') 92 | self.assertLastCall('prepend', empty, 'simple prepend, empty') 93 | # incr 94 | incr = 'incr' 95 | self.assertRaises(pylibmc.NotFound, client.incr, non_existent) 96 | # , 'simple incr, non-existent') 97 | self.assertLastCallRaised('incr', non_existent, 98 | 'simple incr, non-existent') 99 | count = 0 100 | self.assertTrue(client.set(incr, count), 'set initial incr') 101 | self.assertLastCall('set', incr, 'set initial incr') 102 | count += 1 103 | self.assertEquals(client.incr(incr), count, 'simple incr') 104 | self.assertLastCall('incr', incr, 'simple incr') 105 | # decr 106 | self.assertRaises(pylibmc.NotFound, client.decr, non_existent) 107 | # , 'simple decr, non-existent') 108 | self.assertLastCallRaised('decr', non_existent, 109 | 'simple decr, non-existent') 110 | count -= 1 111 | self.assertEquals(client.decr(incr), count, 'simple decr') 112 | self.assertLastCall('decr', incr, 'simple decr') 113 | # delete 114 | self.assertTrue(client.delete(key), 'simple delete') 115 | self.assertLastCall('delete', key, 'simple delete') 116 | self.assertFalse(client.delete(non_existent), 117 | 'simple delete, non-existent') 118 | self.assertLastCall('delete', non_existent, 119 | 'simple delete, non-existent') 120 | # delete_multi (pylibmc implements this as foreach keys delete) 121 | n = memcache_toolbar.panels.instance.size() 122 | self.assertTrue(client.delete_multi(multi_keys), 'delete_multi') 123 | calls = memcache_toolbar.panels.instance.calls() 124 | # before + num_keys + the delete_multi 125 | self.assertEquals(n + len(multi_keys) + 1, len(calls), 126 | 'delete multi call count') 127 | self.assertCall(calls[-4], 'delete_multi', multi_keys, 'delete_multi') 128 | for i, key in enumerate(multi_keys): 129 | self.assertCall(calls[-3 + i], 'delete', key, 130 | 'delete_multi, subsequent delete %d' % i) 131 | 132 | # flush again, this time make sure it works 133 | self.assertTrue(client.flush_all(), 'flush_all') 134 | self.assertLastCall('flush_all', None, 'flush_all') 135 | self.assertEquals(client.get(incr), None, 'flush worked') 136 | self.assertLastCall('get', incr, 'flush worked') 137 | 138 | self.assertEquals(26, memcache_toolbar.panels.instance.size(), 139 | 'total number of calls') 140 | 141 | # test out the panel, mainly resetting 142 | panel = memcache_toolbar.panels.memcache.MemcachePanel() 143 | nav_subtitle = panel.nav_subtitle() 144 | self.assertEquals(nav_subtitle[0:2], '26', 145 | 'pylibmc panel.nav_subtitle') 146 | # reset things 147 | panel.process_request(None) 148 | nav_subtitle = panel.nav_subtitle() 149 | self.assertEquals(nav_subtitle, '0 calls', 150 | 'pylibmc panel.nav_subtitle, post reset') 151 | 152 | 153 | class TestMemcache(TestCase): 154 | 155 | def test_basic(self): 156 | memcache_toolbar.panels.instance.reset() 157 | 158 | client = memcache.Client(['127.0.0.1:11211'], debug=0) 159 | 160 | # flush_all, first so we're in a clean state 161 | client.flush_all() 162 | self.assertLastCall('flush_all', None, 'initial flush') 163 | 164 | # set 165 | key = 'key' 166 | value = 'value' 167 | self.assertTrue(client.set(key, value), 'simple set') 168 | self.assertLastCall('set', key, 'simple set') 169 | # get 170 | self.assertEqual(client.get(key), value, 'simple get') 171 | self.assertLastCall('get', key, 'simple get') 172 | # set_multi 173 | multi = {'key1': 'value1', 'array': ['a1', 'a2'], 'bool': True} 174 | multi_keys = multi.keys() 175 | self.assertEqual(client.set_multi(multi), [], 'set multi') 176 | self.assertLastCall('set_multi', multi_keys, 'set_multi') 177 | # get_multi 178 | self.assertEqual(client.get_multi(multi_keys), multi, 'get_multi') 179 | self.assertLastCall('get_multi', multi_keys, 'get_multi') 180 | # add 181 | add = 'add' 182 | self.assertTrue(client.add(add, value), 'simple add, success') 183 | self.assertLastCall('add', add, 'simple add, success') 184 | self.assertFalse(client.add(add, value), 'simple add, exists') 185 | self.assertLastCall('add', add, 'simple add, exists') 186 | # replace 187 | self.assertTrue(client.replace(add, value), 'simple replace, exists') 188 | self.assertLastCall('replace', add, 'simple replace, exists') 189 | non_existent = 'non-existent' 190 | client.replace(non_existent, value) 191 | self.assertFalse(client.replace(non_existent, value), 192 | 'simple replace, non-existent fails') 193 | self.assertLastCall('replace', non_existent, 194 | 'simple replace, non-existent') 195 | # append 196 | self.assertTrue(client.append(key, value), 'simple append') 197 | self.assertLastCall('append', key, 'simple append, exists') 198 | empty = 'empty' 199 | self.assertFalse(client.append(empty, value), 'simple append, empty') 200 | self.assertLastCall('append', empty, 'simple append, empty') 201 | # prepend 202 | self.assertTrue(client.prepend(key, value), 'simple prepend') 203 | self.assertLastCall('prepend', key, 'simmple prepend, exists') 204 | self.assertFalse(client.prepend(empty, value), 'simple prepend, empty') 205 | self.assertLastCall('prepend', empty, 'simple prepend, empty') 206 | # incr 207 | incr = 'incr' 208 | self.assertFalse(client.incr(non_existent), 209 | 'simple incr, non-existent') 210 | self.assertLastCall('incr', non_existent, 'simple incr, non-existent') 211 | count = 0 212 | self.assertTrue(client.set(incr, count), 'set initial incr') 213 | self.assertLastCall('set', incr, 'set initial incr') 214 | count += 1 215 | self.assertEquals(client.incr(incr), count, 'simple incr') 216 | self.assertLastCall('incr', incr, 'simple incr') 217 | # decr 218 | self.assertFalse(client.decr(non_existent), 219 | 'simple decr, non-existent') 220 | self.assertLastCall('decr', non_existent, 'simple decr, non-existent') 221 | count -= 1 222 | self.assertEquals(client.decr(incr), count, 'simple decr') 223 | self.assertLastCall('decr', incr, 'simple decr') 224 | # delete 225 | self.assertTrue(client.delete(key), 'simple delete') 226 | self.assertLastCall('delete', key, 'simple delete') 227 | self.assertTrue(client.delete(non_existent), 228 | 'simple delete, non-existent') 229 | self.assertLastCall('delete', non_existent, 230 | 'simple delete, non-existent') 231 | # delete_multi (pylibmc implements this as foreach keys delete) 232 | self.assertTrue(client.delete_multi(multi_keys), 'delete_multi') 233 | self.assertLastCall('delete_multi', multi_keys, 'delete_multi') 234 | # flush again, this time make sure it works 235 | client.flush_all() 236 | self.assertLastCall('flush_all', None, 'flush_all') 237 | self.assertEquals(client.get(incr), None, 'flush worked') 238 | self.assertLastCall('get', incr, 'flush worked') 239 | 240 | self.assertEquals(24, memcache_toolbar.panels.instance.size(), 241 | 'total number of calls') 242 | 243 | # test out the panel, mainly resetting 244 | panel = memcache_toolbar.panels.memcache.MemcachePanel() 245 | nav_subtitle = panel.nav_subtitle() 246 | self.assertEquals(nav_subtitle[0:2], '24', 247 | 'memcache panel.nav_subtitle') 248 | # reset things 249 | panel.process_request(None) 250 | nav_subtitle = panel.nav_subtitle() 251 | self.assertEquals(nav_subtitle, '0 calls', 252 | 'memcache panel.nav_subtitle, post reset') 253 | 254 | if __name__ == '__main__': 255 | unittest.main() 256 | --------------------------------------------------------------------------------