├── .gitignore ├── .travis.yml ├── AUTHORS.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── app_metrics ├── __init__.py ├── admin.py ├── backends │ ├── __init__.py │ ├── composite.py │ ├── db.py │ ├── librato.py │ ├── mixpanel.py │ ├── redis.py │ └── statsd.py ├── compat.py ├── exceptions.py ├── locale │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── en │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── metrics_aggregate.py │ │ ├── metrics_send_mail.py │ │ ├── move_to_mixpanel.py │ │ └── move_to_statsd.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_alter_created_to_datetime.py │ ├── 0003_auto__add_gauge.py │ └── __init__.py ├── models.py ├── reports.py ├── tasks.py ├── templates │ └── app_metrics │ │ ├── email.html │ │ └── email.txt ├── tests │ ├── __init__.py │ ├── base_tests.py │ ├── librato_tests.py │ ├── mixpanel_tests.py │ ├── redis_tests.py │ ├── settings.py │ ├── statsd_tests.py │ └── urls.py ├── trending.py ├── urls.py ├── utils.py └── views.py ├── docs ├── Makefile ├── backends.rst ├── changelog.rst ├── conf.py ├── index.rst ├── install.rst ├── make.bat ├── ref │ └── utils.rst ├── settings.rst └── usage.rst ├── requirements └── tests.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | build 4 | dist 5 | MANIFEST 6 | _build 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | before_install: 6 | - export PIP_USE_MIRRORS=true 7 | - sudo apt-get update 8 | - sudo apt-get install redis-server 9 | install: 10 | - pip install -e . 11 | - pip install -r requirements/tests.txt Django==$DJANGO 12 | script: 13 | - django-admin.py test --settings=app_metrics.tests.settings app_metrics 14 | env: 15 | - DJANGO=1.3.7 16 | - DJANGO=1.4.5 17 | - DJANGO=1.5 18 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | django-app-metrics was originally created by Frank Wiles 2 | 3 | Thanks to all of our contributors! 4 | 5 | Ross Poulton 6 | Flavio Curella 7 | Jacob Burch 8 | Jannis Leidel 9 | Flávio Juvena l 10 | Daniel Lindsley 11 | Hannes Struß 12 | Diogo Laginha 13 | Alexander Malaev 14 | Roger Boardman 15 | 16 | Special thanks to RueLaLa.com for sponsoring the additions of the timers, 17 | gauges, and the statsd backend in general 18 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) Frank Wiles 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-app-metrics nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without 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 | 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | recursive-include app_metrics/templates * 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | Django App Metrics 3 | ================== 4 | 5 | .. image:: https://secure.travis-ci.org/frankwiles/django-app-metrics.png 6 | :alt: Build Status 7 | :target: http://travis-ci.org/frankwiles/django-app-metrics 8 | 9 | django-app-metrics allows you to capture and report on various events in your 10 | applications. You simply define various named metrics and record when they 11 | happen. These might be certain events that may be immediatey useful, for 12 | example 'New User Signups', 'Downloads', etc. 13 | 14 | Or they might not prove useful until some point in the future. But if you 15 | begin recording them now you'll have great data later on if you do need it. 16 | 17 | For example 'Total Items Sold' isn't an exciting number when you're just 18 | launching when you only care about revenue, but being able to do a contest 19 | for the 1 millionth sold item in the future you'll be glad you were tracking 20 | it. 21 | 22 | You then group these individual metrics into a MetricSet, where you define 23 | how often you want an email report being sent, and to which User(s) it should 24 | be sent. 25 | 26 | Documentation 27 | ============= 28 | 29 | Documentation can be found at ReadTheDocs_. 30 | 31 | .. _ReadTheDocs: http://django-app-metrics.readthedocs.org/ 32 | 33 | Requirements 34 | ============ 35 | 36 | Celery_ and `django-celery`_ must be installed, however if you do not wish to 37 | actually use Celery you can simply set ``CELERY_ALWAYS_EAGER = True`` in your 38 | settings and it will behave as if Celery was not configured. 39 | 40 | .. _Celery: http://celeryproject.org/ 41 | .. _`django-celery`: http://ask.github.com/django-celery/ 42 | 43 | Django 1.2 and above 44 | 45 | Usage 46 | ===== 47 | 48 | :: 49 | 50 | from app_metrics.utils import create_metric, metric, timing, Timer, gauge 51 | 52 | # Create a new metric to track 53 | my_metric = create_metric(name='New User Metric', slug='new_user_signup') 54 | 55 | # Create a MetricSet which ties a metric to an email schedule and sets 56 | # who should receive it 57 | my_metric_set = create_metric_set(name='My Set', 58 | metrics=[my_metric], 59 | email_recipients=[user1, user2]) 60 | 61 | # Increment the metric by one 62 | metric('new_user_signup') 63 | 64 | # Increment the metric by some other number 65 | metric('new_user_signup', 4) 66 | 67 | # Aggregate metric items into daily, weekly, monthly, and yearly totals 68 | # It's fairly smart about it, so you're safe to run this as often as you 69 | # like 70 | manage.py metrics_aggregate 71 | 72 | # Send email reports to users 73 | manage.py metrics_send_mail 74 | 75 | # Create a timer (only supported in statsd backend currently) 76 | with timing('mytimer'): 77 | for x in some_long_list: 78 | call_time_consuming_function(x) 79 | 80 | # Or if a context manager doesn't work for you you can use a Timer class 81 | t = Timer() 82 | t.start() 83 | something_that_takes_forever() 84 | t.stop() 85 | t.store('mytimer') 86 | 87 | # Gauges are current status type dials (think fuel gauge in a car) 88 | # These simply store and retrieve a value 89 | gauge('current_fuel', '30') 90 | guage('load_load', '3.14') 91 | 92 | Backends 93 | ======== 94 | 95 | ``app_metrics.backends.db`` (Default) - This backend stores all metrics and 96 | aggregations in your database. NOTE: Every call to ``metric()`` generates a 97 | database write, which may decrease your overall performance is you go nuts 98 | with them or have a heavily traffic site. 99 | 100 | ``app_metrics.backends.mixpanel`` - This backend allows you to pipe all of 101 | your calls to ``metric()`` to Mixpanel. See the `Mixpanel documentation`_ 102 | for more information on their API. 103 | 104 | .. _`Mixpanel documentation`: http://mixpanel.com/docs/api-documentation 105 | 106 | ``app_metrics.backends.statsd`` - This backend allows you to pipe all of your 107 | calls to ``metric()`` to a statsd server. See `statsd`_ for more information 108 | on their API. 109 | 110 | .. _`statsd`: https://github.com/etsy/statsd 111 | 112 | ``app_metrics.backends.redis`` - This backend allows you to use the metric() and 113 | gauge() aspects, but not timer aspects of app_metrics. 114 | 115 | ``app_metrics.backends.librato_backend`` - This backend lets you send metrics to 116 | Librato. See the `Librato documentation`_ for more information on their API. 117 | This requires the `Librato library`_. 118 | 119 | .. _`Librato documentation`: http://dev.librato.com/v1/metrics#metrics 120 | .. _`Librato library`: http://pypi.python.org/pypi/librato-metrics 121 | 122 | ``app_metrics.backends.composite`` - This backend lets you compose multiple 123 | backends to which metric-calls are handed. The backends to which the call is 124 | sent can be configured with the ``APP_METRICS_COMPOSITE_BACKENDS`` setting. This 125 | can be overridden in each call by supplying a ``backends`` keyword argument:: 126 | 127 | metric('signups', 42, backends=['app_metrics.backends.librato', 128 | 'app_metrics.backends.db']) 129 | 130 | 131 | Settings 132 | ======== 133 | 134 | ``APP_METRICS_BACKEND`` - Defaults to 'app_metrics.backends.db' if not defined. 135 | 136 | ``APP_METRICS_SEND_ZERO_ACTIVITY`` - Prevent e-mails being sent when there's been 137 | no activity today (i.e. during testing). Defaults to `True`. 138 | 139 | ``APP_METRICS_DISABLED`` - If `True`, do not track metrics, useful for 140 | debugging. Defaults to `False`. 141 | 142 | Mixpanel Settings 143 | ----------------- 144 | Set ``APP_METRICS_BACKEND`` == 'app_metrics.backends.mixpanel'. 145 | 146 | ``APP_METRICS_MIXPANEL_TOKEN`` - Your Mixpanel.com API token 147 | 148 | ``APP_METRICS_MIXPANEL_URL`` - Allow overriding of the API URL end point 149 | 150 | Statsd Settings 151 | --------------- 152 | Set ``APP_METRICS_BACKEND`` == 'app_metrics.backends.statsd'. 153 | 154 | ``APP_METRICS_STATSD_HOST`` - Hostname of statsd server, defaults to 'localhost' 155 | 156 | ``APP_METRICS_STATSD_PORT`` - statsd port, defaults to '8125' 157 | 158 | ``APP_METRICS_STATSD_SAMPLE_RATE`` - statsd sample rate, defaults to 1 159 | 160 | Redis Settings 161 | -------------- 162 | Set ``APP_METRICS_BACKEND`` == 'app_metrics.backends.redis'. 163 | 164 | ``APP_METRICS_REDIS_HOST`` - Hostname of redis server, defaults to 'localhost' 165 | 166 | ``APP_METRICS_REDIS_PORT`` - redis port, defaults to '6379' 167 | 168 | ``APP_METRICS_REDIS_DB`` - redis database number to use, defaults to 0 169 | 170 | Librato Settings 171 | ---------------- 172 | Set ``APP_METRICS_BACKEND`` == 'app_metrics.backends.librato'. 173 | 174 | ``APP_METRICS_LIBRATO_USER`` - Librato username 175 | 176 | ``APP_METRICS_LIBRATO_TOKEN`` - Librato API token 177 | 178 | ``APP_METRICS_LIBRATO_SOURCE`` - Librato data source (e.g. 'staging', 'dev'...) 179 | 180 | Composite Backend Settings 181 | -------------------------- 182 | Set ``APP_METRICS_BACKEND`` == 'app_metrics.backends.composite'. 183 | 184 | ``APP_METRICS_COMPOSITE_BACKENDS`` - List of backends that are used by default, 185 | e.g.:: 186 | 187 | APP_METRICS_COMPOSITE_BACKENDS = ('librato', 'db', 'my_custom_backend',) 188 | 189 | Running the tests 190 | ================= 191 | 192 | To run the tests you'll need some requirements installed, so run:: 193 | 194 | pip install -r requirements/test.txt 195 | 196 | Then simply run:: 197 | 198 | django-admin.py test --settings=app_metrics.tests.settings 199 | 200 | TODO 201 | ---- 202 | 203 | - Improve text and HTML templates to display trending data well 204 | 205 | -------------------------------------------------------------------------------- /app_metrics/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (0, 9, 0) 2 | -------------------------------------------------------------------------------- /app_metrics/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from app_metrics.models import (Metric, MetricSet, MetricItem, MetricDay, 4 | MetricWeek, MetricMonth, MetricYear 5 | ) 6 | 7 | 8 | class MetricAdmin(admin.ModelAdmin): 9 | list_display = ('__unicode__', 'slug', 'num') 10 | list_filter = ['metric__name'] 11 | 12 | def slug(self, obj): 13 | return obj.metric.slug 14 | 15 | admin.site.register(Metric) 16 | admin.site.register(MetricSet) 17 | admin.site.register(MetricDay, MetricAdmin) 18 | admin.site.register(MetricWeek, MetricAdmin) 19 | admin.site.register(MetricMonth, MetricAdmin) 20 | admin.site.register(MetricYear, MetricAdmin) 21 | admin.site.register(MetricItem, MetricAdmin) 22 | -------------------------------------------------------------------------------- /app_metrics/backends/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frankwiles/django-app-metrics/7fc43c73fbd522f271c07d3be308fadf0847c60f/app_metrics/backends/__init__.py -------------------------------------------------------------------------------- /app_metrics/backends/composite.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.utils.importlib import import_module 3 | 4 | DEFAULT_BACKENDS = getattr(settings, 'APP_METRICS_COMPOSITE_BACKENDS', []) 5 | 6 | 7 | def metric(slug, num=1, **kwargs): 8 | _call_backends('metric', slug, num, **kwargs) 9 | 10 | 11 | def timing(slug, seconds_taken, **kwargs): 12 | _call_backends('timing', slug, seconds_taken, **kwargs) 13 | 14 | 15 | def gauge(slug, current_value, **kwargs): 16 | _call_backends('gauge', slug, current_value, **kwargs) 17 | 18 | 19 | def _call_backends(method, slug, value, backends=DEFAULT_BACKENDS, **kwargs): 20 | for path in backends: 21 | backend = import_module(path) 22 | getattr(backend, method)(slug, value, **kwargs) 23 | -------------------------------------------------------------------------------- /app_metrics/backends/db.py: -------------------------------------------------------------------------------- 1 | from app_metrics.tasks import db_metric_task, db_gauge_task 2 | 3 | 4 | def metric(slug, num=1, **kwargs): 5 | """ Fire a celery task to record our metric in the database """ 6 | db_metric_task.delay(slug, num, **kwargs) 7 | 8 | 9 | def timing(slug, seconds_taken, **kwargs): 10 | # Unsupported, hence the noop. 11 | pass 12 | 13 | 14 | def gauge(slug, current_value, **kwargs): 15 | """Fire a celery task to record the gauge's current value in the database.""" 16 | db_gauge_task.delay(slug, current_value, **kwargs) 17 | -------------------------------------------------------------------------------- /app_metrics/backends/librato.py: -------------------------------------------------------------------------------- 1 | from app_metrics.tasks import librato_metric_task 2 | 3 | 4 | def _get_func(async): 5 | return librato_metric_task.delay if async else librato_metric_task 6 | 7 | 8 | def metric(slug, num=1, async=True, **kwargs): 9 | _get_func(async)(slug, num, type="counter", **kwargs) 10 | 11 | 12 | def timing(slug, seconds_taken, async=True, **kwargs): 13 | """not implemented""" 14 | 15 | 16 | def gauge(slug, current_value, async=True, **kwargs): 17 | _get_func(async)(slug, current_value, type="gauge", **kwargs) 18 | -------------------------------------------------------------------------------- /app_metrics/backends/mixpanel.py: -------------------------------------------------------------------------------- 1 | # Backend to handle sending app metrics directly to mixpanel.com 2 | # See http://mixpanel.com/api/docs/ for more information on their API 3 | 4 | from django.conf import settings 5 | from app_metrics.tasks import mixpanel_metric_task 6 | from app_metrics.tasks import _get_token 7 | 8 | 9 | def metric(slug, num=1, properties=None): 10 | """ 11 | Send metric directly to Mixpanel 12 | 13 | - slug here will be used as the Mixpanel "event" string 14 | - if num > 1, we will loop over this and send multiple 15 | - properties are a dictionary of additional information you 16 | may want to pass to Mixpanel. For example you might use it like: 17 | 18 | metric("invite-friends", 19 | properties={"method": "email", "number-friends": "12", "ip": "123.123.123.123"}) 20 | """ 21 | token = _get_token() 22 | mixpanel_metric_task.delay(slug, num, properties) 23 | 24 | 25 | def timing(slug, seconds_taken, **kwargs): 26 | # Unsupported, hence the noop. 27 | pass 28 | 29 | 30 | def gauge(slug, current_value, **kwargs): 31 | # Unsupported, hence the noop. 32 | pass 33 | -------------------------------------------------------------------------------- /app_metrics/backends/redis.py: -------------------------------------------------------------------------------- 1 | # Backend to store info in Redis 2 | from django.conf import settings 3 | from app_metrics.tasks import redis_metric_task, redis_gauge_task 4 | 5 | def metric(slug, num=1, properties={}): 6 | redis_metric_task.delay(slug, num, **properties) 7 | 8 | def timing(slug, seconds_taken, **kwargs): 9 | # No easy way to do this with redis, so this is a no-op 10 | pass 11 | 12 | def gauge(slug, current_value, **kwargs): 13 | redis_gauge_task.delay(slug, current_value, **kwargs) 14 | 15 | -------------------------------------------------------------------------------- /app_metrics/backends/statsd.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from app_metrics.tasks import statsd_metric_task, statsd_timing_task, statsd_gauge_task 3 | 4 | 5 | def metric(slug, num=1, **kwargs): 6 | """ 7 | Send metric directly to statsd 8 | 9 | - ``slug`` will be used as the statsd "bucket" string 10 | - ``num`` increments the counter by that number 11 | """ 12 | statsd_metric_task.delay(slug, num, **kwargs) 13 | 14 | 15 | def timing(slug, seconds_taken, **kwargs): 16 | """ 17 | Send timing directly to statsd 18 | 19 | - ``slug`` will be used as the statsd "bucket" string 20 | - ``seconds_taken`` stores the time taken as a float 21 | """ 22 | statsd_timing_task.delay(slug, seconds_taken, **kwargs) 23 | 24 | 25 | def gauge(slug, current_value, **kwargs): 26 | """ 27 | Send timing directly to statsd 28 | 29 | - ``slug`` will be used as the statsd "bucket" string 30 | - ``current_value`` stores the current value of the gauge 31 | """ 32 | statsd_gauge_task.delay(slug, current_value, **kwargs) 33 | -------------------------------------------------------------------------------- /app_metrics/compat.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | import django 3 | 4 | __all__ = ['User', 'AUTH_USER_MODEL'] 5 | 6 | AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') 7 | 8 | 9 | # Django 1.5+ compatibility 10 | if django.VERSION >= (1, 5): 11 | from django.contrib.auth import get_user_model 12 | User = get_user_model() 13 | username_field = User.USERNAME_FIELD 14 | else: 15 | from django.contrib.auth.models import User 16 | username_field = 'username' 17 | -------------------------------------------------------------------------------- /app_metrics/exceptions.py: -------------------------------------------------------------------------------- 1 | class AppMetricsError(Exception): 2 | pass 3 | 4 | 5 | class InvalidMetricsBackend(AppMetricsError): 6 | pass 7 | 8 | 9 | class MetricError(AppMetricsError): 10 | pass 11 | 12 | 13 | class TimerError(AppMetricsError): 14 | pass 15 | -------------------------------------------------------------------------------- /app_metrics/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frankwiles/django-app-metrics/7fc43c73fbd522f271c07d3be308fadf0847c60f/app_metrics/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /app_metrics/locale/de/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # This file is distributed under the same license as the django-app-metrics package. 2 | # 3 | # Translators: 4 | # Philipp Bosch , 2012. 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: django-app-metrics\n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: 2012-08-05 21:22+0200\n" 10 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 11 | "Last-Translator: Philipp Bosch \n" 12 | "Language-Team: German\n" 13 | "Language: de\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 18 | 19 | #: models.py:11 models.py:36 models.py:135 20 | msgid "name" 21 | msgstr "Name" 22 | 23 | #: models.py:12 models.py:136 24 | msgid "slug" 25 | msgstr "Slug" 26 | 27 | #: models.py:15 models.py:53 models.py:69 models.py:84 models.py:117 28 | msgid "metric" 29 | msgstr "Metrik" 30 | 31 | #: models.py:16 models.py:37 32 | msgid "metrics" 33 | msgstr "Metriken" 34 | 35 | #: models.py:38 36 | msgid "email recipients" 37 | msgstr "E-Mail-Empfänger" 38 | 39 | #: models.py:39 40 | msgid "no e-mail" 41 | msgstr "keine E-Mail" 42 | 43 | #: models.py:40 44 | msgid "send daily" 45 | msgstr "täglich versenden" 46 | 47 | #: models.py:41 48 | msgid "send weekly" 49 | msgstr "wöchentlich versenden" 50 | 51 | #: models.py:42 52 | msgid "send monthly" 53 | msgstr "monatlich versenden" 54 | 55 | #: models.py:45 56 | msgid "metric set" 57 | msgstr "Metrikgruppe" 58 | 59 | #: models.py:46 60 | msgid "metric sets" 61 | msgstr "Metrikgruppen" 62 | 63 | #: models.py:54 models.py:70 models.py:85 models.py:101 models.py:118 64 | msgid "number" 65 | msgstr "Anzahl" 66 | 67 | #: models.py:55 models.py:71 models.py:86 models.py:102 models.py:119 68 | #: models.py:138 69 | msgid "created" 70 | msgstr "erstellt" 71 | 72 | #: models.py:58 73 | msgid "metric item" 74 | msgstr "Metrikelement" 75 | 76 | #: models.py:59 77 | msgid "metric items" 78 | msgstr "Metrikelemente" 79 | 80 | #: models.py:62 81 | #, python-format 82 | msgid "'%(name)s' of %(num)d on %(created)s" 83 | msgstr "'%(name)s' von %(num)d am %(created)s" 84 | 85 | #: models.py:74 86 | msgid "day metric" 87 | msgstr "Tagesmetrik" 88 | 89 | #: models.py:75 90 | msgid "day metrics" 91 | msgstr "Tagesmetriken" 92 | 93 | #: models.py:78 94 | #, python-format 95 | msgid "'%(name)s' for '%(created)s'" 96 | msgstr "'%(name)s' am '%(created)s'" 97 | 98 | #: models.py:89 99 | msgid "week metric" 100 | msgstr "Wochenmetrik" 101 | 102 | #: models.py:90 103 | msgid "week metrics" 104 | msgstr "Wochenmetriken" 105 | 106 | #: models.py:93 107 | #, python-format 108 | msgid "'%(name)s' for week %(week)s of %(year)s" 109 | msgstr "'%(name)s' in KW %(week)s/%(year)s" 110 | 111 | #: models.py:105 112 | msgid "month metric" 113 | msgstr "Monatsmetrik" 114 | 115 | #: models.py:106 116 | msgid "month metrics" 117 | msgstr "Monatsmetriken" 118 | 119 | #: models.py:109 120 | #, python-format 121 | msgid "'%(name)s' for %(month)s %(year)s" 122 | msgstr "'%(name)s' im %(month)s %(year)s" 123 | 124 | #: models.py:122 125 | msgid "year metric" 126 | msgstr "Jahresmetrik" 127 | 128 | #: models.py:123 129 | msgid "year metrics" 130 | msgstr "Jahresmetriken" 131 | 132 | #: models.py:126 133 | #, python-format 134 | msgid "'%(name)s' for %(year)s" 135 | msgstr "'%(name)s' in '%(year)s'" 136 | 137 | #: models.py:137 138 | msgid "current value" 139 | msgstr "aktueller Wert" 140 | 141 | #: models.py:139 142 | msgid "updated" 143 | msgstr "geändert" 144 | 145 | #: models.py:142 146 | msgid "gauge" 147 | msgstr "Wertanzeiger" 148 | 149 | #: models.py:143 150 | msgid "gauges" 151 | msgstr "Wertanzeiger" 152 | 153 | #: management/commands/metrics_send_mail.py:54 154 | #, python-format 155 | msgid "%s Report" 156 | msgstr "%s Bericht" 157 | 158 | #: templates/app_metrics/email.html:3 159 | #, python-format 160 | msgid "%(name)s report for %(date)s" 161 | msgstr "%(name)s Bericht für %(date)s" 162 | 163 | #: templates/app_metrics/email.html:5 164 | msgid "today so far" 165 | msgstr "heute bislang" 166 | 167 | #: templates/app_metrics/email.html:15 templates/app_metrics/email.html:19 168 | msgid "yesterday" 169 | msgstr "gestern" 170 | 171 | #: templates/app_metrics/email.html:20 172 | msgid "prev week" 173 | msgstr "letzte Woche" 174 | 175 | #: templates/app_metrics/email.html:21 176 | msgid "prev month" 177 | msgstr "letzter Monat" 178 | 179 | #: templates/app_metrics/email.html:33 templates/app_metrics/email.html:37 180 | msgid "week to date" 181 | msgstr "diese Woche bislang" 182 | 183 | #: templates/app_metrics/email.html:38 184 | msgid "last week" 185 | msgstr "letzte Woche" 186 | 187 | #: templates/app_metrics/email.html:39 188 | msgid "this week last month" 189 | msgstr "diese Woche im letzten Monat" 190 | 191 | #: templates/app_metrics/email.html:40 192 | msgid "this week last year" 193 | msgstr "diese Woche letztes Jahr" 194 | 195 | #: templates/app_metrics/email.html:54 templates/app_metrics/email.html:58 196 | msgid "month to date" 197 | msgstr "dieser Monat bislang" 198 | 199 | #: templates/app_metrics/email.html:59 200 | msgid "last month" 201 | msgstr "letzter Monat" 202 | 203 | #: templates/app_metrics/email.html:60 204 | msgid "this month last year" 205 | msgstr "dieser Monat letztes Jahr" 206 | 207 | #: templates/app_metrics/email.html:72 templates/app_metrics/email.html:76 208 | msgid "year to date" 209 | msgstr "dieses Jahr bislang" 210 | 211 | #: templates/app_metrics/email.html:77 212 | msgid "last year" 213 | msgstr "letztes Jahr" 214 | -------------------------------------------------------------------------------- /app_metrics/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frankwiles/django-app-metrics/7fc43c73fbd522f271c07d3be308fadf0847c60f/app_metrics/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /app_metrics/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2012-08-05 20:02+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: models.py:11 models.py:36 models.py:135 21 | msgid "name" 22 | msgstr "" 23 | 24 | #: models.py:12 models.py:136 25 | msgid "slug" 26 | msgstr "" 27 | 28 | #: models.py:15 models.py:53 models.py:69 models.py:84 models.py:117 29 | msgid "metric" 30 | msgstr "" 31 | 32 | #: models.py:16 models.py:37 33 | msgid "metrics" 34 | msgstr "" 35 | 36 | #: models.py:38 37 | msgid "email recipients" 38 | msgstr "" 39 | 40 | #: models.py:39 41 | msgid "no e-mail" 42 | msgstr "" 43 | 44 | #: models.py:40 45 | msgid "send daily" 46 | msgstr "" 47 | 48 | #: models.py:41 49 | msgid "send weekly" 50 | msgstr "" 51 | 52 | #: models.py:42 53 | msgid "send monthly" 54 | msgstr "" 55 | 56 | #: models.py:45 57 | msgid "metric set" 58 | msgstr "" 59 | 60 | #: models.py:46 61 | msgid "metric sets" 62 | msgstr "" 63 | 64 | #: models.py:54 models.py:70 models.py:85 models.py:101 models.py:118 65 | msgid "number" 66 | msgstr "" 67 | 68 | #: models.py:55 models.py:71 models.py:86 models.py:102 models.py:119 69 | #: models.py:138 70 | msgid "created" 71 | msgstr "" 72 | 73 | #: models.py:58 74 | msgid "metric item" 75 | msgstr "" 76 | 77 | #: models.py:59 78 | msgid "metric items" 79 | msgstr "" 80 | 81 | #: models.py:62 82 | #, python-format 83 | msgid "'%(name)s' of %(num)d on %(created)s" 84 | msgstr "" 85 | 86 | #: models.py:74 87 | msgid "day metric" 88 | msgstr "" 89 | 90 | #: models.py:75 91 | msgid "day metrics" 92 | msgstr "" 93 | 94 | #: models.py:78 95 | #, python-format 96 | msgid "'%(name)s' for '%(created)s'" 97 | msgstr "" 98 | 99 | #: models.py:89 100 | msgid "week metric" 101 | msgstr "" 102 | 103 | #: models.py:90 104 | msgid "week metrics" 105 | msgstr "" 106 | 107 | #: models.py:93 108 | #, python-format 109 | msgid "'%(name)s' for week %(week)s of %(year)s" 110 | msgstr "" 111 | 112 | #: models.py:105 113 | msgid "month metric" 114 | msgstr "" 115 | 116 | #: models.py:106 117 | msgid "month metrics" 118 | msgstr "" 119 | 120 | #: models.py:109 121 | #, python-format 122 | msgid "'%(name)s' for %(month)s %(year)s" 123 | msgstr "" 124 | 125 | #: models.py:122 126 | msgid "year metric" 127 | msgstr "" 128 | 129 | #: models.py:123 130 | msgid "year metrics" 131 | msgstr "" 132 | 133 | #: models.py:126 134 | #, python-format 135 | msgid "'%(name)s' for month of '%(year)s'" 136 | msgstr "" 137 | 138 | #: models.py:137 139 | msgid "current value" 140 | msgstr "" 141 | 142 | #: models.py:139 143 | msgid "updated" 144 | msgstr "" 145 | 146 | #: models.py:142 147 | msgid "gauge" 148 | msgstr "" 149 | 150 | #: models.py:143 151 | msgid "gauges" 152 | msgstr "" 153 | -------------------------------------------------------------------------------- /app_metrics/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frankwiles/django-app-metrics/7fc43c73fbd522f271c07d3be308fadf0847c60f/app_metrics/management/__init__.py -------------------------------------------------------------------------------- /app_metrics/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frankwiles/django-app-metrics/7fc43c73fbd522f271c07d3be308fadf0847c60f/app_metrics/management/commands/__init__.py -------------------------------------------------------------------------------- /app_metrics/management/commands/metrics_aggregate.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from django.core.management.base import NoArgsCommand 3 | 4 | from app_metrics.models import Metric, MetricItem, MetricDay, MetricWeek, MetricMonth, MetricYear 5 | 6 | from app_metrics.utils import week_for_date, month_for_date, year_for_date, get_backend 7 | 8 | class Command(NoArgsCommand): 9 | help = "Aggregate Application Metrics" 10 | 11 | requires_model_validation = True 12 | 13 | def handle_noargs(self, **options): 14 | """ Aggregate Application Metrics """ 15 | 16 | backend = get_backend() 17 | 18 | # If using Mixpanel this command is a NOOP 19 | if backend == 'app_metrics.backends.mixpanel': 20 | print "Useless use of metrics_aggregate when using Mixpanel backend" 21 | return 22 | 23 | # Aggregate Items 24 | items = MetricItem.objects.all() 25 | 26 | for i in items: 27 | # Daily Aggregation 28 | day,create = MetricDay.objects.get_or_create(metric=i.metric, 29 | created=i.created) 30 | 31 | day.num = day.num + i.num 32 | day.save() 33 | 34 | # Weekly Aggregation 35 | week_date = week_for_date(i.created) 36 | week, create = MetricWeek.objects.get_or_create(metric=i.metric, 37 | created=week_date) 38 | 39 | week.num = week.num + i.num 40 | week.save() 41 | 42 | # Monthly Aggregation 43 | month_date = month_for_date(i.created) 44 | month, create = MetricMonth.objects.get_or_create(metric=i.metric, 45 | created=month_date) 46 | month.num = month.num + i.num 47 | month.save() 48 | 49 | # Yearly Aggregation 50 | year_date = year_for_date(i.created) 51 | year, create = MetricYear.objects.get_or_create(metric=i.metric, 52 | created=year_date) 53 | year.num = year.num + i.num 54 | year.save() 55 | 56 | # Kill off our items 57 | items.delete() 58 | -------------------------------------------------------------------------------- /app_metrics/management/commands/metrics_send_mail.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import string 3 | 4 | from django.core.management.base import NoArgsCommand 5 | from django.conf import settings 6 | from django.db.models import Q 7 | from django.utils import translation 8 | from django.utils.translation import ugettext_lazy as _ 9 | 10 | from app_metrics.reports import generate_report 11 | from app_metrics.models import MetricSet, Metric 12 | from app_metrics.utils import get_backend 13 | 14 | class Command(NoArgsCommand): 15 | help = "Send Report E-mails" 16 | requires_model_validation = True 17 | can_import_settings = True 18 | 19 | def handle_noargs(self, **options): 20 | """ Send Report E-mails """ 21 | 22 | from django.conf import settings 23 | translation.activate(settings.LANGUAGE_CODE) 24 | 25 | backend = get_backend() 26 | 27 | # This command is a NOOP if using the Mixpanel backend 28 | if backend == 'app_metrics.backends.mixpanel': 29 | print "Useless use of metrics_send_email when using Mixpanel backend." 30 | return 31 | 32 | # Determine if we should also send any weekly or monthly reports 33 | today = datetime.date.today() 34 | if today.weekday == 0: 35 | send_weekly = True 36 | else: 37 | send_weekly = False 38 | 39 | if today.day == 1: 40 | send_monthly = True 41 | else: 42 | send_monthly = False 43 | 44 | qs = MetricSet.objects.filter(Q(no_email=False), Q(send_daily=True) | Q(send_monthly=send_monthly) | Q(send_weekly=send_weekly)) 45 | 46 | if "mailer" in settings.INSTALLED_APPS: 47 | from mailer import send_html_mail 48 | USE_MAILER = True 49 | else: 50 | from django.core.mail import EmailMultiAlternatives 51 | USE_MAILER = False 52 | 53 | for s in qs: 54 | subject = _("%s Report") % s.name 55 | 56 | recipient_list = s.email_recipients.values_list('email', flat=True) 57 | 58 | (message, message_html) = generate_report(s, html=True) 59 | 60 | if message == None: 61 | continue 62 | 63 | if USE_MAILER: 64 | send_html_mail(subject=subject, 65 | message=message, 66 | message_html=message_html, 67 | from_email=settings.DEFAULT_FROM_EMAIL, 68 | recipient_list=recipient_list) 69 | else: 70 | msg = EmailMultiAlternatives(subject=subject, 71 | body=message, 72 | from_email=settings.DEFAULT_FROM_EMAIL, 73 | to=recipient_list) 74 | msg.attach_alternative(message_html, "text/html") 75 | msg.send() 76 | 77 | translation.deactivate() 78 | 79 | -------------------------------------------------------------------------------- /app_metrics/management/commands/move_to_mixpanel.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import NoArgsCommand 2 | 3 | from app_metrics.models import MetricItem 4 | from app_metrics.backends.mixpanel import metric 5 | from app_metrics.utils import get_backend 6 | 7 | class Command(NoArgsCommand): 8 | help = "Move MetricItems from the db backend to MixPanel" 9 | 10 | requires_model_validation = True 11 | 12 | def handle_noargs(self, **options): 13 | """ Move MetricItems from the db backend to MixPanel" """ 14 | 15 | backend = get_backend() 16 | 17 | # If not using Mixpanel this command is a NOOP 18 | if backend != 'app_metrics.backends.mixpanel': 19 | print "You need to set the backend to MixPanel" 20 | return 21 | 22 | items = MetricItem.objects.all() 23 | 24 | for i in items: 25 | properties = { 26 | 'time': i.created.strftime('%s'), 27 | } 28 | metric(i.metric.slug, num=i.num, properties=properties) 29 | 30 | # Kill off our items 31 | items.delete() 32 | -------------------------------------------------------------------------------- /app_metrics/management/commands/move_to_statsd.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from django.core.management.base import NoArgsCommand 3 | from app_metrics.models import MetricItem 4 | from app_metrics.backends.statsd_backend import metric 5 | 6 | 7 | class Command(NoArgsCommand): 8 | help = "Move MetricItems from the db backend to statsd" 9 | requires_model_validation = True 10 | 11 | def handle_noargs(self, **options): 12 | """Move MetricItems from the db backend to statsd""" 13 | backend = get_backend() 14 | 15 | # If not using statsd, this command is a NOOP. 16 | if backend != 'app_metrics.backends.statsd_backend': 17 | sys.exit(1, "You need to set the backend to 'statsd_backend'") 18 | 19 | items = MetricItem.objects.all() 20 | 21 | for i in items: 22 | metric(i.metric.slug, num=i.num) 23 | 24 | # Kill off our items 25 | items.delete() 26 | -------------------------------------------------------------------------------- /app_metrics/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | from app_metrics.compat import AUTH_USER_MODEL 8 | 9 | 10 | class Migration(SchemaMigration): 11 | 12 | def forwards(self, orm): 13 | # Adding model 'Metric' 14 | db.create_table('app_metrics_metric', ( 15 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 16 | ('name', self.gf('django.db.models.fields.CharField')(max_length=50)), 17 | ('slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=60)), 18 | )) 19 | db.send_create_signal('app_metrics', ['Metric']) 20 | 21 | # Adding model 'MetricSet' 22 | db.create_table('app_metrics_metricset', ( 23 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 24 | ('name', self.gf('django.db.models.fields.CharField')(max_length=50)), 25 | ('no_email', self.gf('django.db.models.fields.BooleanField')(default=False)), 26 | ('send_daily', self.gf('django.db.models.fields.BooleanField')(default=True)), 27 | ('send_weekly', self.gf('django.db.models.fields.BooleanField')(default=False)), 28 | ('send_monthly', self.gf('django.db.models.fields.BooleanField')(default=False)), 29 | )) 30 | db.send_create_signal('app_metrics', ['MetricSet']) 31 | 32 | # Adding M2M table for field metrics on 'MetricSet' 33 | db.create_table('app_metrics_metricset_metrics', ( 34 | ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), 35 | ('metricset', models.ForeignKey(orm['app_metrics.metricset'], null=False)), 36 | ('metric', models.ForeignKey(orm['app_metrics.metric'], null=False)) 37 | )) 38 | db.create_unique('app_metrics_metricset_metrics', ['metricset_id', 'metric_id']) 39 | 40 | # Adding M2M table for field email_recipients on 'MetricSet' 41 | db.create_table('app_metrics_metricset_email_recipients', ( 42 | ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), 43 | ('metricset', models.ForeignKey(orm['app_metrics.metricset'], null=False)), 44 | ('user', models.ForeignKey(orm[AUTH_USER_MODEL], null=False)) 45 | )) 46 | db.create_unique('app_metrics_metricset_email_recipients', ['metricset_id', 'user_id']) 47 | 48 | # Adding model 'MetricItem' 49 | db.create_table('app_metrics_metricitem', ( 50 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 51 | ('metric', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['app_metrics.Metric'])), 52 | ('num', self.gf('django.db.models.fields.IntegerField')(default=1)), 53 | ('created', self.gf('django.db.models.fields.DateField')(default=datetime.date.today)), 54 | )) 55 | db.send_create_signal('app_metrics', ['MetricItem']) 56 | 57 | # Adding model 'MetricDay' 58 | db.create_table('app_metrics_metricday', ( 59 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 60 | ('metric', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['app_metrics.Metric'])), 61 | ('num', self.gf('django.db.models.fields.BigIntegerField')(default=0)), 62 | ('created', self.gf('django.db.models.fields.DateField')(default=datetime.date.today)), 63 | )) 64 | db.send_create_signal('app_metrics', ['MetricDay']) 65 | 66 | # Adding model 'MetricWeek' 67 | db.create_table('app_metrics_metricweek', ( 68 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 69 | ('metric', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['app_metrics.Metric'])), 70 | ('num', self.gf('django.db.models.fields.BigIntegerField')(default=0)), 71 | ('created', self.gf('django.db.models.fields.DateField')(default=datetime.date.today)), 72 | )) 73 | db.send_create_signal('app_metrics', ['MetricWeek']) 74 | 75 | # Adding model 'MetricMonth' 76 | db.create_table('app_metrics_metricmonth', ( 77 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 78 | ('metric', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['app_metrics.Metric'])), 79 | ('num', self.gf('django.db.models.fields.BigIntegerField')(default=0)), 80 | ('created', self.gf('django.db.models.fields.DateField')(default=datetime.date.today)), 81 | )) 82 | db.send_create_signal('app_metrics', ['MetricMonth']) 83 | 84 | # Adding model 'MetricYear' 85 | db.create_table('app_metrics_metricyear', ( 86 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 87 | ('metric', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['app_metrics.Metric'])), 88 | ('num', self.gf('django.db.models.fields.BigIntegerField')(default=0)), 89 | ('created', self.gf('django.db.models.fields.DateField')(default=datetime.date.today)), 90 | )) 91 | db.send_create_signal('app_metrics', ['MetricYear']) 92 | 93 | def backwards(self, orm): 94 | # Deleting model 'Metric' 95 | db.delete_table('app_metrics_metric') 96 | 97 | # Deleting model 'MetricSet' 98 | db.delete_table('app_metrics_metricset') 99 | 100 | # Removing M2M table for field metrics on 'MetricSet' 101 | db.delete_table('app_metrics_metricset_metrics') 102 | 103 | # Removing M2M table for field email_recipients on 'MetricSet' 104 | db.delete_table('app_metrics_metricset_email_recipients') 105 | 106 | # Deleting model 'MetricItem' 107 | db.delete_table('app_metrics_metricitem') 108 | 109 | # Deleting model 'MetricDay' 110 | db.delete_table('app_metrics_metricday') 111 | 112 | # Deleting model 'MetricWeek' 113 | db.delete_table('app_metrics_metricweek') 114 | 115 | # Deleting model 'MetricMonth' 116 | db.delete_table('app_metrics_metricmonth') 117 | 118 | # Deleting model 'MetricYear' 119 | db.delete_table('app_metrics_metricyear') 120 | 121 | models = { 122 | 'app_metrics.metric': { 123 | 'Meta': {'object_name': 'Metric'}, 124 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 125 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 126 | 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '60'}) 127 | }, 128 | 'app_metrics.metricday': { 129 | 'Meta': {'object_name': 'MetricDay'}, 130 | 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}), 131 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 132 | 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), 133 | 'num': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) 134 | }, 135 | 'app_metrics.metricitem': { 136 | 'Meta': {'object_name': 'MetricItem'}, 137 | 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime.now'}), 138 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 139 | 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), 140 | 'num': ('django.db.models.fields.IntegerField', [], {'default': '1'}) 141 | }, 142 | 'app_metrics.metricmonth': { 143 | 'Meta': {'object_name': 'MetricMonth'}, 144 | 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}), 145 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 146 | 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), 147 | 'num': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) 148 | }, 149 | 'app_metrics.metricset': { 150 | 'Meta': {'object_name': 'MetricSet'}, 151 | 'email_recipients': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm[AUTH_USER_MODEL]", 'symmetrical': 'False'}), 152 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 153 | 'metrics': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['app_metrics.Metric']", 'symmetrical': 'False'}), 154 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 155 | 'no_email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 156 | 'send_daily': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 157 | 'send_monthly': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 158 | 'send_weekly': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) 159 | }, 160 | 'app_metrics.metricweek': { 161 | 'Meta': {'object_name': 'MetricWeek'}, 162 | 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}), 163 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 164 | 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), 165 | 'num': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) 166 | }, 167 | 'app_metrics.metricyear': { 168 | 'Meta': {'object_name': 'MetricYear'}, 169 | 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}), 170 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 171 | 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), 172 | 'num': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) 173 | }, 174 | 'auth.group': { 175 | 'Meta': {'object_name': 'Group'}, 176 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 177 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 178 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 179 | }, 180 | 'auth.permission': { 181 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 182 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 183 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 184 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 185 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 186 | }, 187 | AUTH_USER_MODEL: { 188 | 'Meta': {'object_name': AUTH_USER_MODEL.split('.')[-1]}, 189 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 190 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 191 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 192 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 193 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 194 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 195 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 196 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 197 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 198 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 199 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 200 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 201 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 202 | }, 203 | 'contenttypes.contenttype': { 204 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 205 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 206 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 207 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 208 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 209 | } 210 | } 211 | 212 | complete_apps = ['app_metrics'] 213 | -------------------------------------------------------------------------------- /app_metrics/migrations/0002_alter_created_to_datetime.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | from app_metrics.compat import AUTH_USER_MODEL 8 | 9 | 10 | class Migration(SchemaMigration): 11 | 12 | def forwards(self, orm): 13 | 14 | # Changing field 'MetricItem.created' 15 | db.alter_column('app_metrics_metricitem', 'created', self.gf('django.db.models.fields.DateTimeField')()) 16 | def backwards(self, orm): 17 | 18 | # Changing field 'MetricItem.created' 19 | db.alter_column('app_metrics_metricitem', 'created', self.gf('django.db.models.fields.DateField')()) 20 | models = { 21 | 'app_metrics.metric': { 22 | 'Meta': {'object_name': 'Metric'}, 23 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 24 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 25 | 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '60'}) 26 | }, 27 | 'app_metrics.metricday': { 28 | 'Meta': {'object_name': 'MetricDay'}, 29 | 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}), 30 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 31 | 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), 32 | 'num': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) 33 | }, 34 | 'app_metrics.metricitem': { 35 | 'Meta': {'object_name': 'MetricItem'}, 36 | 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 37 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 38 | 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), 39 | 'num': ('django.db.models.fields.IntegerField', [], {'default': '1'}) 40 | }, 41 | 'app_metrics.metricmonth': { 42 | 'Meta': {'object_name': 'MetricMonth'}, 43 | 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}), 44 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 45 | 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), 46 | 'num': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) 47 | }, 48 | 'app_metrics.metricset': { 49 | 'Meta': {'object_name': 'MetricSet'}, 50 | 'email_recipients': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm[AUTH_USER_MODEL]", 'symmetrical': 'False'}), 51 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 52 | 'metrics': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['app_metrics.Metric']", 'symmetrical': 'False'}), 53 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 54 | 'no_email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 55 | 'send_daily': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 56 | 'send_monthly': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 57 | 'send_weekly': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) 58 | }, 59 | 'app_metrics.metricweek': { 60 | 'Meta': {'object_name': 'MetricWeek'}, 61 | 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}), 62 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 63 | 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), 64 | 'num': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) 65 | }, 66 | 'app_metrics.metricyear': { 67 | 'Meta': {'object_name': 'MetricYear'}, 68 | 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}), 69 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 70 | 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), 71 | 'num': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) 72 | }, 73 | 'auth.group': { 74 | 'Meta': {'object_name': 'Group'}, 75 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 76 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 77 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 78 | }, 79 | 'auth.permission': { 80 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 81 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 82 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 83 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 84 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 85 | }, 86 | AUTH_USER_MODEL: { 87 | 'Meta': {'object_name': AUTH_USER_MODEL.split('.')[-1]}, 88 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 89 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 90 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 91 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 92 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 93 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 94 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 95 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 96 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 97 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 98 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 99 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 100 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 101 | }, 102 | 'contenttypes.contenttype': { 103 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 104 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 105 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 106 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 107 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 108 | } 109 | } 110 | 111 | complete_apps = ['app_metrics'] 112 | -------------------------------------------------------------------------------- /app_metrics/migrations/0003_auto__add_gauge.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | from app_metrics.compat import AUTH_USER_MODEL 8 | 9 | 10 | class Migration(SchemaMigration): 11 | 12 | def forwards(self, orm): 13 | # Adding model 'Gauge' 14 | db.create_table('app_metrics_gauge', ( 15 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 16 | ('name', self.gf('django.db.models.fields.CharField')(max_length=50)), 17 | ('slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=60)), 18 | ('current_value', self.gf('django.db.models.fields.DecimalField')(default='0.00', max_digits=15, decimal_places=6)), 19 | ('created', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)), 20 | ('updated', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)), 21 | )) 22 | db.send_create_signal('app_metrics', ['Gauge']) 23 | 24 | 25 | def backwards(self, orm): 26 | # Deleting model 'Gauge' 27 | db.delete_table('app_metrics_gauge') 28 | 29 | 30 | models = { 31 | 'app_metrics.gauge': { 32 | 'Meta': {'object_name': 'Gauge'}, 33 | 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 34 | 'current_value': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '15', 'decimal_places': '6'}), 35 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 36 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 37 | 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '60'}), 38 | 'updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}) 39 | }, 40 | 'app_metrics.metric': { 41 | 'Meta': {'object_name': 'Metric'}, 42 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 43 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 44 | 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '60'}) 45 | }, 46 | 'app_metrics.metricday': { 47 | 'Meta': {'object_name': 'MetricDay'}, 48 | 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}), 49 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 50 | 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), 51 | 'num': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) 52 | }, 53 | 'app_metrics.metricitem': { 54 | 'Meta': {'object_name': 'MetricItem'}, 55 | 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 56 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 57 | 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), 58 | 'num': ('django.db.models.fields.IntegerField', [], {'default': '1'}) 59 | }, 60 | 'app_metrics.metricmonth': { 61 | 'Meta': {'object_name': 'MetricMonth'}, 62 | 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}), 63 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 64 | 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), 65 | 'num': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) 66 | }, 67 | 'app_metrics.metricset': { 68 | 'Meta': {'object_name': 'MetricSet'}, 69 | 'email_recipients': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm[AUTH_USER_MODEL]", 'symmetrical': 'False'}), 70 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 71 | 'metrics': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['app_metrics.Metric']", 'symmetrical': 'False'}), 72 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), 73 | 'no_email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 74 | 'send_daily': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 75 | 'send_monthly': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 76 | 'send_weekly': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) 77 | }, 78 | 'app_metrics.metricweek': { 79 | 'Meta': {'object_name': 'MetricWeek'}, 80 | 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}), 81 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 82 | 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), 83 | 'num': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) 84 | }, 85 | 'app_metrics.metricyear': { 86 | 'Meta': {'object_name': 'MetricYear'}, 87 | 'created': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}), 88 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 89 | 'metric': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app_metrics.Metric']"}), 90 | 'num': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}) 91 | }, 92 | 'auth.group': { 93 | 'Meta': {'object_name': 'Group'}, 94 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 95 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 96 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 97 | }, 98 | 'auth.permission': { 99 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 100 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 101 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 102 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 103 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 104 | }, 105 | AUTH_USER_MODEL: { 106 | 'Meta': {'object_name': AUTH_USER_MODEL.split('.')[-1]}, 107 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 108 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 109 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 110 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 111 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 112 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 113 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 114 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 115 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 116 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 117 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 118 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 119 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 120 | }, 121 | 'contenttypes.contenttype': { 122 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 123 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 124 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 125 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 126 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 127 | } 128 | } 129 | 130 | complete_apps = ['app_metrics'] 131 | -------------------------------------------------------------------------------- /app_metrics/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frankwiles/django-app-metrics/7fc43c73fbd522f271c07d3be308fadf0847c60f/app_metrics/migrations/__init__.py -------------------------------------------------------------------------------- /app_metrics/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.db import models, IntegrityError 4 | from django.template.defaultfilters import slugify 5 | from django.utils.translation import ugettext_lazy as _ 6 | 7 | from app_metrics.compat import User 8 | 9 | try: 10 | from django.db.transaction import atomic 11 | except ImportError: 12 | # Django < 1.6 use noop context manager 13 | from contextlib import contextmanager 14 | atomic = contextmanager(lambda:(yield)) 15 | 16 | 17 | class Metric(models.Model): 18 | """ The type of metric we want to store """ 19 | name = models.CharField(_('name'), max_length=50) 20 | slug = models.SlugField(_('slug'), unique=True, max_length=60, db_index=True) 21 | 22 | class Meta: 23 | verbose_name = _('metric') 24 | verbose_name_plural = _('metrics') 25 | 26 | def __unicode__(self): 27 | return self.name 28 | 29 | def save(self, *args, **kwargs): 30 | if not self.id and not self.slug: 31 | self.slug = slugify(self.name) 32 | i = 0 33 | while True: 34 | try: 35 | with atomic(): 36 | return super(Metric, self).save(*args, **kwargs) 37 | except IntegrityError: 38 | i += 1 39 | self.slug = "%s_%d" % (self.slug, i) 40 | else: 41 | return super(Metric, self).save(*args, **kwargs) 42 | 43 | 44 | class MetricSet(models.Model): 45 | """ A set of metrics that should be sent via email to certain users """ 46 | name = models.CharField(_('name'), max_length=50) 47 | metrics = models.ManyToManyField(Metric, verbose_name=_('metrics')) 48 | email_recipients = models.ManyToManyField(User, verbose_name=_('email recipients')) 49 | no_email = models.BooleanField(_('no e-mail'), default=False) 50 | send_daily = models.BooleanField(_('send daily'), default=True) 51 | send_weekly = models.BooleanField(_('send weekly'), default=False) 52 | send_monthly = models.BooleanField(_('send monthly'), default=False) 53 | 54 | class Meta: 55 | verbose_name = _('metric set') 56 | verbose_name_plural = _('metric sets') 57 | 58 | def __unicode__(self): 59 | return self.name 60 | 61 | 62 | class MetricItem(models.Model): 63 | """ Individual metric items """ 64 | metric = models.ForeignKey(Metric, verbose_name=_('metric')) 65 | num = models.IntegerField(_('number'), default=1) 66 | created = models.DateTimeField(_('created'), default=datetime.datetime.now) 67 | 68 | class Meta: 69 | verbose_name = _('metric item') 70 | verbose_name_plural = _('metric items') 71 | 72 | def __unicode__(self): 73 | return _("'%(name)s' of %(num)d on %(created)s") % { 74 | 'name': self.metric.name, 75 | 'num': self.num, 76 | 'created': self.created 77 | } 78 | 79 | 80 | class MetricDay(models.Model): 81 | """ Aggregation of Metrics on a per day basis """ 82 | metric = models.ForeignKey(Metric, verbose_name=_('metric')) 83 | num = models.BigIntegerField(_('number'), default=0) 84 | created = models.DateField(_('created'), default=datetime.date.today) 85 | 86 | class Meta: 87 | verbose_name = _('day metric') 88 | verbose_name_plural = _('day metrics') 89 | 90 | def __unicode__(self): 91 | return _("'%(name)s' for '%(created)s'") % { 92 | 'name': self.metric.name, 93 | 'created': self.created 94 | } 95 | 96 | 97 | class MetricWeek(models.Model): 98 | """ Aggregation of Metrics on a weekly basis """ 99 | metric = models.ForeignKey(Metric, verbose_name=_('metric')) 100 | num = models.BigIntegerField(_('number'), default=0) 101 | created = models.DateField(_('created'), default=datetime.date.today) 102 | 103 | class Meta: 104 | verbose_name = _('week metric') 105 | verbose_name_plural = _('week metrics') 106 | 107 | def __unicode__(self): 108 | return _("'%(name)s' for week %(week)s of %(year)s") % { 109 | 'name': self.metric.name, 110 | 'week': self.created.strftime("%U"), 111 | 'year': self.created.strftime("%Y") 112 | } 113 | 114 | 115 | class MetricMonth(models.Model): 116 | """ Aggregation of Metrics on monthly basis """ 117 | metric = models.ForeignKey(Metric, verbose_name=('metric')) 118 | num = models.BigIntegerField(_('number'), default=0) 119 | created = models.DateField(_('created'), default=datetime.date.today) 120 | 121 | class Meta: 122 | verbose_name = _('month metric') 123 | verbose_name_plural = _('month metrics') 124 | 125 | def __unicode__(self): 126 | return _("'%(name)s' for %(month)s %(year)s") % { 127 | 'name': self.metric.name, 128 | 'month': self.created.strftime("%B"), 129 | 'year': self.created.strftime("%Y") 130 | } 131 | 132 | 133 | class MetricYear(models.Model): 134 | """ Aggregation of Metrics on a yearly basis """ 135 | metric = models.ForeignKey(Metric, verbose_name=_('metric')) 136 | num = models.BigIntegerField(_('number'), default=0) 137 | created = models.DateField(_('created'), default=datetime.date.today) 138 | 139 | class Meta: 140 | verbose_name = _('year metric') 141 | verbose_name_plural = _('year metrics') 142 | 143 | def __unicode__(self): 144 | return _("'%(name)s' for %(year)s") % { 145 | 'name': self.metric.name, 146 | 'year': self.created.strftime("%Y") 147 | } 148 | 149 | 150 | class Gauge(models.Model): 151 | """ 152 | A representation of the current state of some data. 153 | """ 154 | name = models.CharField(_('name'), max_length=50) 155 | slug = models.SlugField(_('slug'), unique=True, max_length=60) 156 | current_value = models.DecimalField(_('current value'), max_digits=15, decimal_places=6, default='0.00') 157 | created = models.DateTimeField(_('created'), default=datetime.datetime.now) 158 | updated = models.DateTimeField(_('updated'), default=datetime.datetime.now) 159 | 160 | class Meta: 161 | verbose_name = _('gauge') 162 | verbose_name_plural = _('gauges') 163 | 164 | def __unicode__(self): 165 | return self.name 166 | 167 | def save(self, *args, **kwargs): 168 | if not self.id and not self.slug: 169 | self.slug = slugify(self.name) 170 | 171 | self.updated = datetime.datetime.now() 172 | return super(Gauge, self).save(*args, **kwargs) 173 | -------------------------------------------------------------------------------- /app_metrics/reports.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.template.loader import render_to_string 4 | 5 | from app_metrics.models import * 6 | from app_metrics.trending import trending_for_metric 7 | 8 | from django.conf import settings 9 | 10 | def generate_report(metric_set=None, html=False): 11 | """ Generate a Metric Set Report """ 12 | 13 | # Get trending data for each metric 14 | metric_trends = [] 15 | for m in metric_set.metrics.all(): 16 | data = {'metric': m} 17 | data['trends'] = trending_for_metric(m) 18 | metric_trends.append(data) 19 | 20 | send_zero_activity = getattr(settings, 'APP_METRICS_SEND_ZERO_ACTIVITY', True) 21 | 22 | if not send_zero_activity: 23 | activity_today = False 24 | for trend in metric_trends: 25 | if trend['trends']['current_day'] > 0: 26 | activity_today = True 27 | continue 28 | if not activity_today: 29 | return None, None 30 | 31 | 32 | message = render_to_string('app_metrics/email.txt', { 33 | 'metric_set': metric_set, 34 | 'metrics': metric_trends, 35 | 'today': datetime.date.today(), 36 | }) 37 | 38 | if html: 39 | message_html = render_to_string('app_metrics/email.html', { 40 | 'metric_set': metric_set, 41 | 'metrics': metric_trends, 42 | 'today': datetime.date.today(), 43 | }) 44 | 45 | return message, message_html 46 | 47 | else: 48 | return message 49 | -------------------------------------------------------------------------------- /app_metrics/tasks.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import urllib 4 | import urllib2 5 | import datetime 6 | 7 | try: 8 | from celery.task import task 9 | except ImportError: 10 | from celery.decorators import task 11 | 12 | from django.conf import settings 13 | from django.core.exceptions import ImproperlyConfigured 14 | 15 | from app_metrics.models import Metric, MetricItem, Gauge 16 | 17 | # For statsd support 18 | try: 19 | # Not required. If we do this once at the top of the module, we save 20 | # ourselves the pain of importing every time the task fires. 21 | import statsd 22 | except ImportError: 23 | statsd = None 24 | 25 | # For redis support 26 | try: 27 | import redis 28 | except: 29 | redis = None 30 | 31 | # For librato support 32 | try: 33 | import librato 34 | from librato.metrics import Gauge as LibratoGauge 35 | from librato.metrics import Counter as LibratoCounter 36 | except ImportError: 37 | librato = None 38 | 39 | 40 | class MixPanelTrackError(Exception): 41 | pass 42 | 43 | # DB Tasks 44 | 45 | @task 46 | def db_metric_task(slug, num=1, **kwargs): 47 | met = Metric.objects.get(slug=slug) 48 | MetricItem.objects.create(metric=met, num=num) 49 | 50 | 51 | @task 52 | def db_gauge_task(slug, current_value, **kwargs): 53 | gauge, created = Gauge.objects.get_or_create(slug=slug, defaults={ 54 | 'name': slug, 55 | 'current_value': current_value, 56 | }) 57 | 58 | if not created: 59 | gauge.current_value = current_value 60 | gauge.save() 61 | 62 | 63 | def _get_token(): 64 | token = getattr(settings, 'APP_METRICS_MIXPANEL_TOKEN', None) 65 | 66 | if not token: 67 | raise ImproperlyConfigured("You must define APP_METRICS_MIXPANEL_TOKEN when using the mixpanel backend.") 68 | else: 69 | return token 70 | 71 | # Mixpanel tasks 72 | 73 | @task 74 | def mixpanel_metric_task(slug, num, properties=None, **kwargs): 75 | token = _get_token() 76 | if properties is None: 77 | properties = dict() 78 | 79 | if "token" not in properties: 80 | properties["token"] = token 81 | 82 | url = getattr(settings, 'APP_METRICS_MIXPANEL_API_URL', "http://api.mixpanel.com/track/") 83 | 84 | params = {"event": slug, "properties": properties} 85 | b64_data = base64.b64encode(json.dumps(params)) 86 | 87 | data = urllib.urlencode({"data": b64_data}) 88 | req = urllib2.Request(url, data) 89 | for i in range(num): 90 | response = urllib2.urlopen(req) 91 | if response.read() == '0': 92 | raise MixPanelTrackError(u'MixPanel returned 0') 93 | 94 | 95 | # Statsd tasks 96 | 97 | def get_statsd_conn(): 98 | if statsd is None: 99 | raise ImproperlyConfigured("You must install 'python-statsd' in order to use this backend.") 100 | 101 | conn = statsd.Connection( 102 | host=getattr(settings, 'APP_METRICS_STATSD_HOST', 'localhost'), 103 | port=int(getattr(settings, 'APP_METRICS_STATSD_PORT', 8125)), 104 | sample_rate=float(getattr(settings, 'APP_METRICS_STATSD_SAMPLE_RATE', 1)), 105 | ) 106 | return conn 107 | 108 | 109 | @task 110 | def statsd_metric_task(slug, num=1, **kwargs): 111 | conn = get_statsd_conn() 112 | counter = statsd.Counter(slug, connection=conn) 113 | counter += num 114 | 115 | 116 | @task 117 | def statsd_timing_task(slug, seconds_taken=1.0, **kwargs): 118 | conn = get_statsd_conn() 119 | 120 | # You might be wondering "Why not use ``timer.start/.stop`` here?" 121 | # The problem is that this is a task, likely running out of process 122 | # & perhaps with network overhead. We'll measure the timing elsewhere, 123 | # in-process, to be as accurate as possible, then use the out-of-process 124 | # task for talking to the statsd backend. 125 | timer = statsd.Timer(slug, connection=conn) 126 | timer.send('total', seconds_taken) 127 | 128 | 129 | @task 130 | def statsd_gauge_task(slug, current_value, **kwargs): 131 | conn = get_statsd_conn() 132 | gauge = statsd.Gauge(slug, connection=conn) 133 | # We send nothing here, since we only have one name/slug to work with here. 134 | gauge.send('', current_value) 135 | 136 | # Redis tasks 137 | 138 | def get_redis_conn(): 139 | if redis is None: 140 | raise ImproperlyConfigured("You must install 'redis' in order to use this backend.") 141 | conn = redis.StrictRedis( 142 | host=getattr(settings, 'APP_METRICS_REDIS_HOST', 'localhost'), 143 | port=getattr(settings, 'APP_METRICS_REDIS_PORT', 6379), 144 | db=getattr(settings, 'APP_METRICS_REDIS_DB', 0), 145 | ) 146 | return conn 147 | 148 | 149 | @task 150 | def redis_metric_task(slug, num=1, **kwargs): 151 | # Record a metric in redis. We prefix our key here with 'm' for Metric 152 | # and build keys for each day, week, month, and year 153 | r = get_redis_conn() 154 | 155 | # Build keys 156 | now = datetime.datetime.now() 157 | day_key = "m:%s:%s" % (slug, now.strftime("%Y-%m-%d")) 158 | week_key = "m:%s:w:%s" % (slug, now.strftime("%U")) 159 | month_key = "m:%s:m:%s" % (slug, now.strftime("%Y-%m")) 160 | year_key = "m:%s:y:%s" % (slug, now.strftime("%Y")) 161 | 162 | # Increment keys 163 | r.incrby(day_key, num) 164 | r.incrby(week_key, num) 165 | r.incrby(month_key, num) 166 | r.incrby(year_key, num) 167 | 168 | 169 | @task 170 | def redis_gauge_task(slug, current_value, **kwargs): 171 | # We prefix our keys with a 'g' here for Gauge to avoid issues 172 | # of having a gauge and metric of the same name 173 | r = get_redis_conn() 174 | r.set("g:%s" % slug, current_value) 175 | 176 | # Librato tasks 177 | 178 | @task 179 | def librato_metric_task(name, num, **kwargs): 180 | api = librato.connect(settings.APP_METRICS_LIBRATO_USER, 181 | settings.APP_METRICS_LIBRATO_TOKEN) 182 | source = settings.APP_METRICS_LIBRATO_SOURCE 183 | api.submit(name, num, source=source, **kwargs) 184 | -------------------------------------------------------------------------------- /app_metrics/templates/app_metrics/email.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 |

{% blocktrans with metric_set.name as name and today|date:"D d M Y" as date %}{{ name }} report for {{ date }}{% endblocktrans %}

4 | 5 |

{% trans "today so far"|capfirst %}

6 | 7 | {% for m in metrics %} 8 | 9 | 10 | 11 | 12 | {% endfor %} 13 |
{{ m.metric.name }}{{ m.trends.current_day }}
14 | 15 |

{% trans "yesterday"|capfirst %}

16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% for m in metrics %} 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% endfor %} 31 |
{% trans "yesterday"|capfirst %}{% trans "prev week"|capfirst %}{% trans "prev month"|capfirst %}
{{ m.metric.name }}{{ m.trends.yesterday.yesterday }}{{ m.trends.yesterday.previous_week }}{{ m.trends.yesterday.previous_month }}
32 | 33 |

{% trans "week to date"|capfirst %}

34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {% for m in metrics %} 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | {% endfor %} 51 |
{% trans "week to date"|capfirst %}{% trans "last week"|capfirst %}{% trans "this week last month"|capfirst %}{% trans "this week last year"|capfirst %}
{{ m.metric.name }}{{ m.trends.week.week }}{{ m.trends.week.previous_week }}{{ m.trends.week.previous_month_week }}{{ m.trends.week.previous_year_week }}
52 | 53 |

{% trans "month to date"|capfirst %}

54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | {% for m in metrics %} 62 | 63 | 64 | 65 | 66 | 67 | 68 | {% endfor %} 69 |
{% trans "month to date"|capfirst %}{% trans "last month"|capfirst %}{% trans "this month last year"|capfirst %}
{{ m.metric.name }}{{ m.trends.month.month }}{{ m.trends.month.previous_month }}{{ m.trends.month.previous_year_month }}
70 | 71 |

{% trans "year to date"|capfirst %}

72 | 73 | 74 | 75 | 76 | 77 | 78 | {% for m in metrics %} 79 | 80 | 81 | 82 | 83 | 84 | {% endfor %} 85 |
{% trans "year to date"|capfirst %}{% trans "last year"|capfirst %}
{{ m.metric.name }}{{ m.trends.year.year }}{{ m.trends.year.previous_year }}
86 | 87 | 88 | -------------------------------------------------------------------------------- /app_metrics/templates/app_metrics/email.txt: -------------------------------------------------------------------------------- 1 | 2 | {{ metric_set.name }} Report for {{ today|date:"D d M Y" }} 3 | ----------------------------------------------------------- 4 | 5 | TODAY:{% for m in metrics %} 6 | {{ m.metric.name|ljust:40 }} {{ m.trends.current_day|rjust:10 }} {% endfor %} 7 | 8 | YESTERDAY Yesterday Prev Week Prev Month 9 | --------- ---------- ---------- ----------{% for m in metrics %} 10 | {{ m.metric.name|ljust:40 }} {{ m.trends.yesterday.yesterday|rjust:10 }} {{ m.trends.yesterday.previous_week|rjust:10 }} {{ m.trends.yesterday.previous_month|rjust:10 }}{% endfor %} 11 | 12 | WEEK TO DATE This Week Prev Week Prev Month Prev Year 13 | ----------- ---------- ---------- ---------- ----------{% for m in metrics %} 14 | {{ m.metric.name|ljust:40 }} {{ m.trends.week.week|rjust:10 }} {{ m.trends.week.previous_week|rjust:10 }} {{ m.trends.week.previous_month_week|rjust:10 }} {{ m.trends.week.previous_year_week|rjust:10 }}{% endfor %} 15 | 16 | THIS MONTH This Month Prev Month Prev Year 17 | ---------- ---------- ---------- ----------{% for m in metrics %} 18 | {{ m.metric.name|ljust:40 }} {{ m.trends.month.month|rjust:10 }} {{ m.trends.month.previous_month|rjust:10 }} {{ m.trends.month.previous_year_month|rjust:10 }}{% endfor %} 19 | 20 | THIS YEAR This Year Last Year 21 | --------- ---------- ----------{% for m in metrics %} 22 | {{ m.metric.name|ljust:40 }} {{ m.trends.year.year|rjust:10 }} {{ m.trends.year.previous_year|rjust:10 }}{% endfor %} 23 | 24 | -------------------------------------------------------------------------------- /app_metrics/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from app_metrics.tests.base_tests import * 2 | from app_metrics.tests.mixpanel_tests import * 3 | 4 | try: 5 | import statsd 6 | except ImportError: 7 | print "Skipping the statsd tests." 8 | statsd = None 9 | 10 | if statsd is not None: 11 | from app_metrics.tests.statsd_tests import * 12 | 13 | try: 14 | import redis 15 | except ImportError: 16 | print "Skipping redis tests." 17 | redis = None 18 | 19 | if redis is not None: 20 | from app_metrics.tests.redis_tests import * 21 | 22 | #try: 23 | # import librato 24 | #except ImportError: 25 | # print "Skipping librato tests..." 26 | # librato = None 27 | # 28 | #if librato is not None: 29 | # from app_metrics.tests.librato_tests import * 30 | -------------------------------------------------------------------------------- /app_metrics/tests/base_tests.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from decimal import Decimal 3 | import mock 4 | 5 | from django.test import TestCase 6 | from django.core import management 7 | from django.conf import settings 8 | from django.core import mail 9 | from django.contrib.auth.models import User 10 | from django.core.exceptions import ImproperlyConfigured 11 | 12 | from app_metrics.exceptions import TimerError 13 | from app_metrics.models import Metric, MetricItem, MetricDay, MetricWeek, MetricMonth, MetricYear, Gauge 14 | from app_metrics.utils import * 15 | from app_metrics.trending import _trending_for_current_day, _trending_for_yesterday, _trending_for_week, _trending_for_month, _trending_for_year 16 | 17 | class MetricCreationTests(TestCase): 18 | 19 | def test_auto_slug_creation(self): 20 | new_metric = Metric.objects.create(name='foo bar') 21 | self.assertEqual(new_metric.name, 'foo bar') 22 | self.assertEqual(new_metric.slug, 'foo-bar') 23 | 24 | new_metric2 = Metric.objects.create(name='foo bar') 25 | self.assertEqual(new_metric2.name, 'foo bar') 26 | self.assertEqual(new_metric2.slug, 'foo-bar_1') 27 | 28 | def test_metric(self): 29 | new_metric = create_metric(name='Test Metric Class', 30 | slug='test_metric') 31 | 32 | metric('test_metric') 33 | metric('test_metric') 34 | metric('test_metric') 35 | 36 | current_count = MetricItem.objects.filter(metric=new_metric) 37 | 38 | self.assertEqual(len(current_count), 3) 39 | self.assertEqual(current_count[0].num, 1) 40 | self.assertEqual(current_count[1].num, 1) 41 | self.assertEqual(current_count[2].num, 1) 42 | 43 | def test_get_or_create_metric(self): 44 | new_metric = get_or_create_metric(name='Test Metric Class', 45 | slug='test_metric') 46 | 47 | metric('test_metric') 48 | metric('test_metric') 49 | metric('test_metric') 50 | 51 | new_metric = get_or_create_metric(name='Test Metric Class', 52 | slug='test_metric') 53 | 54 | current_count = MetricItem.objects.filter(metric=new_metric) 55 | self.assertEqual(len(current_count), 3) 56 | self.assertEqual(current_count[0].num, 1) 57 | self.assertEqual(current_count[1].num, 1) 58 | self.assertEqual(current_count[2].num, 1) 59 | 60 | class MetricAggregationTests(TestCase): 61 | 62 | def setUp(self): 63 | self.metric1 = create_metric(name='Test Aggregation1', slug='test_agg1') 64 | self.metric2 = create_metric(name='Test Aggregation2', slug='test_agg2') 65 | 66 | metric('test_agg1') 67 | metric('test_agg1') 68 | 69 | metric('test_agg2') 70 | metric('test_agg2') 71 | metric('test_agg2') 72 | 73 | def test_daily_aggregation(self): 74 | management.call_command('metrics_aggregate') 75 | 76 | day1 = MetricDay.objects.get(metric=self.metric1) 77 | day2 = MetricDay.objects.get(metric=self.metric2) 78 | self.assertEqual(day1.num, 2) 79 | self.assertEqual(day2.num, 3) 80 | 81 | def test_weekly_aggregation(self): 82 | management.call_command('metrics_aggregate') 83 | 84 | week1 = MetricWeek.objects.get(metric=self.metric1) 85 | week2 = MetricWeek.objects.get(metric=self.metric2) 86 | self.assertEqual(week1.num, 2) 87 | self.assertEqual(week2.num, 3) 88 | 89 | def test_monthly_aggregation(self): 90 | management.call_command('metrics_aggregate') 91 | 92 | month1 = MetricMonth.objects.get(metric=self.metric1) 93 | month2 = MetricMonth.objects.get(metric=self.metric2) 94 | self.assertEqual(month1.num, 2) 95 | self.assertEqual(month2.num, 3) 96 | 97 | def test_yearly_aggregation(self): 98 | management.call_command('metrics_aggregate') 99 | 100 | year1 = MetricYear.objects.get(metric=self.metric1) 101 | year2 = MetricYear.objects.get(metric=self.metric2) 102 | self.assertEqual(year1.num, 2) 103 | self.assertEqual(year2.num, 3) 104 | 105 | class DisabledTests(TestCase): 106 | """ Test disabling collection """ 107 | 108 | def setUp(self): 109 | super(DisabledTests, self).setUp() 110 | self.old_disabled = getattr(settings, 'APP_METRICS_DISABLED', False) 111 | settings.APP_METRICS_DISABLED = True 112 | self.metric1 = create_metric(name='Test Disable', slug='test_disable') 113 | 114 | def test_disabled(self): 115 | self.assertEqual(MetricItem.objects.filter(metric__slug='test_disable').count(), 0) 116 | settings.APP_METRICS_DISABLED = True 117 | metric('test_disable') 118 | self.assertEqual(MetricItem.objects.filter(metric__slug='test_disable').count(), 0) 119 | self.assertTrue(collection_disabled()) 120 | 121 | def tearDown(self): 122 | settings.APP_METRICS_DISABLED = self.old_disabled 123 | super(DisabledTests, self).tearDown() 124 | 125 | class TrendingTests(TestCase): 126 | """ Test that our trending logic works """ 127 | 128 | def setUp(self): 129 | self.metric1 = create_metric(name='Test Trending1', slug='test_trend1') 130 | self.metric2 = create_metric(name='Test Trending2', slug='test_trend2') 131 | 132 | def test_trending_for_current_day(self): 133 | """ Test current day trending counter """ 134 | metric('test_trend1') 135 | metric('test_trend1') 136 | management.call_command('metrics_aggregate') 137 | metric('test_trend1') 138 | metric('test_trend1') 139 | 140 | count = _trending_for_current_day(self.metric1) 141 | self.assertEqual(count, 4) 142 | 143 | def test_trending_for_yesterday(self): 144 | """ Test yesterday trending """ 145 | today = datetime.date.today() 146 | yesterday_date = today - datetime.timedelta(days=1) 147 | previous_week_date = today - datetime.timedelta(weeks=1) 148 | previous_month_date = get_previous_month(today) 149 | 150 | MetricDay.objects.create(metric=self.metric1, num=5, created=yesterday_date) 151 | MetricDay.objects.create(metric=self.metric1, num=4, created=previous_week_date) 152 | MetricDay.objects.create(metric=self.metric1, num=3, created=previous_month_date) 153 | 154 | data = _trending_for_yesterday(self.metric1) 155 | self.assertEqual(data['yesterday'], 5) 156 | self.assertEqual(data['previous_week'], 4) 157 | self.assertEqual(data['previous_month'], 3) 158 | 159 | def test_trending_for_week(self): 160 | """ Test weekly trending data """ 161 | this_week_date = week_for_date(datetime.date.today()) 162 | previous_week_date = this_week_date - datetime.timedelta(weeks=1) 163 | previous_month_date = get_previous_month(this_week_date) 164 | previous_year_date = get_previous_year(this_week_date) 165 | 166 | MetricWeek.objects.create(metric=self.metric1, num=5, created=this_week_date) 167 | MetricWeek.objects.create(metric=self.metric1, num=4, created=previous_week_date) 168 | MetricWeek.objects.create(metric=self.metric1, num=3, created=previous_month_date) 169 | MetricWeek.objects.create(metric=self.metric1, num=2, created=previous_year_date) 170 | 171 | data = _trending_for_week(self.metric1) 172 | self.assertEqual(data['week'], 5) 173 | self.assertEqual(data['previous_week'], 4) 174 | self.assertEqual(data['previous_month_week'], 3) 175 | self.assertEqual(data['previous_year_week'], 2) 176 | 177 | def test_trending_for_month(self): 178 | """ Test monthly trending data """ 179 | this_month_date = month_for_date(datetime.date.today()) 180 | previous_month_date = get_previous_month(this_month_date) 181 | previous_month_year_date = get_previous_year(this_month_date) 182 | 183 | MetricMonth.objects.create(metric=self.metric1, num=5, created=this_month_date) 184 | MetricMonth.objects.create(metric=self.metric1, num=4, created=previous_month_date) 185 | MetricMonth.objects.create(metric=self.metric1, num=3, created=previous_month_year_date) 186 | 187 | data = _trending_for_month(self.metric1) 188 | self.assertEqual(data['month'], 5) 189 | self.assertEqual(data['previous_month'], 4) 190 | self.assertEqual(data['previous_month_year'], 3) 191 | 192 | def test_trending_for_year(self): 193 | """ Test yearly trending data """ 194 | this_year_date = year_for_date(datetime.date.today()) 195 | previous_year_date = get_previous_year(this_year_date) 196 | 197 | MetricYear.objects.create(metric=self.metric1, num=5, created=this_year_date) 198 | MetricYear.objects.create(metric=self.metric1, num=4, created=previous_year_date) 199 | 200 | data = _trending_for_year(self.metric1) 201 | self.assertEqual(data['year'], 5) 202 | self.assertEqual(data['previous_year'], 4) 203 | 204 | def test_missing_trending(self): 205 | this_week_date = week_for_date(datetime.date.today()) 206 | previous_week_date = this_week_date - datetime.timedelta(weeks=1) 207 | previous_month_date = get_previous_month(this_week_date) 208 | previous_year_date = get_previous_year(this_week_date) 209 | 210 | MetricWeek.objects.create(metric=self.metric1, num=5, created=this_week_date) 211 | MetricWeek.objects.create(metric=self.metric1, num=4, created=previous_week_date) 212 | MetricWeek.objects.create(metric=self.metric1, num=3, created=previous_month_date) 213 | 214 | data = _trending_for_week(self.metric1) 215 | self.assertEqual(data['week'], 5) 216 | self.assertEqual(data['previous_week'], 4) 217 | self.assertEqual(data['previous_month_week'], 3) 218 | self.assertEqual(data['previous_year_week'], 0) 219 | 220 | class EmailTests(TestCase): 221 | """ Test that our emails send properly """ 222 | def setUp(self): 223 | self.user1 = User.objects.create_user('user1', 'user1@example.com', 'user1pass') 224 | self.user2 = User.objects.create_user('user2', 'user2@example.com', 'user2pass') 225 | self.metric1 = create_metric(name='Test Trending1', slug='test_trend1') 226 | self.metric2 = create_metric(name='Test Trending2', slug='test_trend2') 227 | self.set = create_metric_set(name="Fake Report", 228 | metrics=[self.metric1, self.metric2], 229 | email_recipients=[self.user1, self.user2]) 230 | 231 | def test_email(self): 232 | """ Test email sending """ 233 | metric('test_trend1') 234 | metric('test_trend1') 235 | metric('test_trend1') 236 | metric('test_trend2') 237 | metric('test_trend2') 238 | 239 | management.call_command('metrics_aggregate') 240 | management.call_command('metrics_send_mail') 241 | 242 | self.assertEqual(len(mail.outbox), 1) 243 | 244 | 245 | class GaugeTests(TestCase): 246 | def setUp(self): 247 | self.gauge = Gauge.objects.create( 248 | name='Testing', 249 | ) 250 | 251 | def test_existing_gauge(self): 252 | self.assertEqual(Gauge.objects.all().count(), 1) 253 | self.assertEqual(Gauge.objects.get(slug='testing').current_value, Decimal('0.00')) 254 | gauge('testing', '10.5') 255 | 256 | # We should not have created a new gauge 257 | self.assertEqual(Gauge.objects.all().count(), 1) 258 | self.assertEqual(Gauge.objects.get(slug='testing').current_value, Decimal('10.5')) 259 | 260 | # Test updating 261 | gauge('testing', '11.1') 262 | self.assertEqual(Gauge.objects.get(slug='testing').current_value, Decimal('11.1')) 263 | 264 | def test_new_gauge(self): 265 | gauge('test_trend1', Decimal('12.373')) 266 | self.assertEqual(Gauge.objects.all().count(), 2) 267 | self.assertTrue('test_trend1' in list(Gauge.objects.all().values_list('slug', flat=True))) 268 | self.assertEqual(Gauge.objects.get(slug='test_trend1').current_value, Decimal('12.373')) 269 | 270 | 271 | class TimerTests(TestCase): 272 | def setUp(self): 273 | super(TimerTests, self).setUp() 274 | self.timer = Timer() 275 | 276 | def test_start(self): 277 | with mock.patch('time.time') as mock_time: 278 | mock_time.return_value = '12345.0' 279 | self.timer.start() 280 | 281 | self.assertEqual(self.timer._start, '12345.0') 282 | 283 | self.assertRaises(TimerError, self.timer.start) 284 | 285 | def test_stop(self): 286 | self.assertRaises(TimerError, self.timer.stop) 287 | 288 | with mock.patch('time.time') as mock_time: 289 | mock_time.return_value = 12345.0 290 | self.timer.start() 291 | 292 | with mock.patch('time.time') as mock_time: 293 | mock_time.return_value = 12347.2 294 | self.timer.stop() 295 | 296 | self.assertAlmostEqual(self.timer._elapsed, 2.2) 297 | self.assertEqual(self.timer._start, None) 298 | 299 | def test_elapsed(self): 300 | self.assertRaises(TimerError, self.timer.elapsed) 301 | 302 | self.timer._elapsed = 2.2 303 | self.assertEqual(self.timer.elapsed(), 2.2) 304 | 305 | # The ``Timer.store()`` is tested as part of the statsd backend tests. 306 | 307 | class MixpanelCommandTest1(TestCase): 308 | """ Test out our management command noops """ 309 | 310 | def setUp(self): 311 | new_metric = Metric.objects.create(name='foo bar') 312 | i = MetricItem.objects.create(metric=new_metric) 313 | self.old_backend = settings.APP_METRICS_BACKEND 314 | settings.APP_METRICS_BACKEND = 'app_metrics.backends.db' 315 | 316 | def test_mixpanel_noop(self): 317 | self.assertEqual(1, MetricItem.objects.all().count()) 318 | management.call_command('move_to_mixpanel') 319 | self.assertEqual(1, MetricItem.objects.all().count()) 320 | 321 | def tearDown(self): 322 | settings.APP_METRICS_BACKEND = self.old_backend 323 | 324 | class MixpanelCommandTest2(TestCase): 325 | """ Test out our management command works """ 326 | 327 | def setUp(self): 328 | new_metric = Metric.objects.create(name='foo bar') 329 | i = MetricItem.objects.create(metric=new_metric) 330 | self.old_backend = settings.APP_METRICS_BACKEND 331 | settings.APP_METRICS_BACKEND = 'app_metrics.backends.mixpanel' 332 | 333 | def test_mixpanel_op(self): 334 | self.assertEqual(1, MetricItem.objects.all().count()) 335 | self.assertRaises(ImproperlyConfigured, management.call_command, 'move_to_mixpanel') 336 | 337 | def tearDown(self): 338 | settings.APP_METRICS_BACKEND = self.old_backend 339 | -------------------------------------------------------------------------------- /app_metrics/tests/librato_tests.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frankwiles/django-app-metrics/7fc43c73fbd522f271c07d3be308fadf0847c60f/app_metrics/tests/librato_tests.py -------------------------------------------------------------------------------- /app_metrics/tests/mixpanel_tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.conf import settings 3 | from django.core.exceptions import ImproperlyConfigured 4 | 5 | from app_metrics.utils import * 6 | 7 | class MixpanelMetricConfigTests(TestCase): 8 | 9 | def setUp(self): 10 | self.old_backend = settings.APP_METRICS_BACKEND 11 | settings.APP_METRICS_BACKEND = 'app_metrics.backends.mixpanel' 12 | 13 | def test_metric(self): 14 | self.assertRaises(ImproperlyConfigured, metric, 'test_metric') 15 | 16 | def tearDown(self): 17 | settings.APP_METRICS_BACKEND = self.old_backend 18 | 19 | class MixpanelCreationTests(TestCase): 20 | 21 | def setUp(self): 22 | self.old_backend = settings.APP_METRICS_BACKEND 23 | self.old_token = settings.APP_METRICS_MIXPANEL_TOKEN 24 | settings.APP_METRICS_BACKEND = 'app_metrics.backends.mixpanel' 25 | settings.APP_METRICS_MIXPANEL_TOKEN = 'foobar' 26 | 27 | def test_metric(self): 28 | metric('testing') 29 | 30 | def tearDown(self): 31 | settings.APP_METRICS_BACKEND = self.old_backend 32 | settings.APP_METRICS_MIXPANEL_TOKEN = self.old_token 33 | -------------------------------------------------------------------------------- /app_metrics/tests/redis_tests.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from django.test import TestCase 3 | from django.conf import settings 4 | from app_metrics.utils import metric, gauge 5 | 6 | class RedisTests(TestCase): 7 | def setUp(self): 8 | super(RedisTests, self).setUp() 9 | self.old_backend = getattr(settings, 'APP_METRICS_BACKEND', None) 10 | settings.APP_METRICS_BACKEND = 'app_metrics.backends.redis' 11 | 12 | def tearDown(self): 13 | settings.APP_METRICS_BACKEND = self.old_backend 14 | super(RedisTests, self).tearDown() 15 | 16 | def test_metric(self): 17 | with mock.patch('redis.client.StrictRedis') as mock_client: 18 | instance = mock_client.return_value 19 | instance._send.return_value = 1 20 | 21 | metric('foo') 22 | mock_client._send.asert_called_with(mock.ANY, {'slug':'foo', 'num':'1'}) 23 | 24 | def test_gauge(self): 25 | with mock.patch('redis.client.StrictRedis') as mock_client: 26 | instance = mock_client.return_value 27 | instance._send.return_value = 1 28 | 29 | gauge('testing', 10.5) 30 | mock_client._send.asert_called_with(mock.ANY, {'slug':'testing', 'current_value':'10.5'}) 31 | 32 | -------------------------------------------------------------------------------- /app_metrics/tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import django 4 | 5 | BASE_PATH = os.path.dirname(__file__) 6 | 7 | if django.VERSION[:2] >= (1, 3): 8 | DATABASES = { 9 | 'default': { 10 | 'ENGINE': 'django.db.backends.sqlite3', 11 | 'NAME': ':memory:', 12 | } 13 | } 14 | else: 15 | DATABASE_ENGINE = 'sqlite3' 16 | DATABASE_NAME = ':memory:' 17 | 18 | SITE_ID = 1 19 | 20 | DEBUG = True 21 | 22 | TEST_RUNNER = 'django_coverage.coverage_runner.CoverageRunner' 23 | 24 | COVERAGE_MODULE_EXCLUDES = [ 25 | 'tests$', 'settings$', 'urls$', 26 | 'common.views.test', '__init__', 'django', 27 | 'migrations', 'djcelery' 28 | ] 29 | 30 | COVERAGE_REPORT_HTML_OUTPUT_DIR = os.path.join(BASE_PATH, 'coverage') 31 | 32 | INSTALLED_APPS = [ 33 | 'django.contrib.admin', 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'django.contrib.sessions', 37 | 'django.contrib.sites', 38 | 'app_metrics', 39 | 'app_metrics.tests', 40 | 'djcelery', 41 | 'django_coverage' 42 | ] 43 | 44 | ROOT_URLCONF = 'app_metrics.tests.urls' 45 | 46 | CELERY_ALWAYS_EAGER = True 47 | 48 | APP_METRICS_BACKEND = 'app_metrics.backends.db' 49 | APP_METRICS_MIXPANEL_TOKEN = None 50 | APP_METRICS_DISABLED = False 51 | 52 | SECRET_KEY = "herp-derp" 53 | -------------------------------------------------------------------------------- /app_metrics/tests/statsd_tests.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | import mock 3 | import time 4 | from django.test import TestCase 5 | from django.conf import settings 6 | from app_metrics.utils import metric, timing, gauge 7 | 8 | 9 | class StatsdCreationTests(TestCase): 10 | def setUp(self): 11 | super(StatsdCreationTests, self).setUp() 12 | self.old_backend = getattr(settings, 'APP_METRICS_BACKEND', None) 13 | settings.APP_METRICS_BACKEND = 'app_metrics.backends.statsd' 14 | 15 | def test_metric(self): 16 | with mock.patch('statsd.Client') as mock_client: 17 | instance = mock_client.return_value 18 | instance._send.return_value = 1 19 | 20 | metric('testing') 21 | mock_client._send.assert_called_with(mock.ANY, {'testing': '1|c'}) 22 | 23 | metric('testing', 2) 24 | mock_client._send.assert_called_with(mock.ANY, {'testing': '2|c'}) 25 | 26 | metric('another', 4) 27 | mock_client._send.assert_called_with(mock.ANY, {'another': '4|c'}) 28 | 29 | def test_timing(self): 30 | with mock.patch('statsd.Client') as mock_client: 31 | instance = mock_client.return_value 32 | instance._send.return_value = 1 33 | 34 | with timing('testing'): 35 | time.sleep(0.025) 36 | 37 | mock_client._send.assert_called_with(mock.ANY, {'testing.total': mock.ANY}) 38 | 39 | def test_gauge(self): 40 | with mock.patch('statsd.Client') as mock_client: 41 | instance = mock_client.return_value 42 | instance._send.return_value = 1 43 | 44 | gauge('testing', 10.5) 45 | mock_client._send.assert_called_with(mock.ANY, {'testing': '10.5|g'}) 46 | 47 | gauge('testing', Decimal('6.576')) 48 | mock_client._send.assert_called_with(mock.ANY, {'testing': '6.576|g'}) 49 | 50 | gauge('another', 1) 51 | mock_client._send.assert_called_with(mock.ANY, {'another': '1|g'}) 52 | 53 | def tearDown(self): 54 | settings.APP_METRICS_BACKEND = self.old_backend 55 | super(StatsdCreationTests, self).tearDown() 56 | -------------------------------------------------------------------------------- /app_metrics/tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import * 2 | from django.conf import settings 3 | from django.contrib import admin 4 | 5 | admin.autodiscover() 6 | 7 | urlpatterns = patterns('', 8 | (r'^admin/', include(admin.site.urls)), 9 | (r'^admin/metrics/', include('app_metrics.urls')), 10 | ) 11 | -------------------------------------------------------------------------------- /app_metrics/trending.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.core.exceptions import ObjectDoesNotExist 4 | 5 | from app_metrics.models import Metric, MetricItem, MetricDay, MetricWeek, MetricMonth, MetricYear 6 | from app_metrics.utils import week_for_date, month_for_date, year_for_date, get_previous_month, get_previous_year 7 | 8 | class InvalidMetric(Exception): pass 9 | 10 | def trending_for_metric(metric=None, date=None): 11 | """ Build a dictionary of trending values for a given metric """ 12 | 13 | if not isinstance(metric, Metric): 14 | raise InvalidMetric('No Metric instance passed to trending_for_metric()') 15 | if not date: 16 | date = datetime.date.today() 17 | 18 | data = {} 19 | 20 | # Get current day values so far 21 | if date == datetime.date.today(): 22 | data['current_day'] = _trending_for_current_day(metric) 23 | 24 | data['yesterday'] = _trending_for_yesterday(metric) 25 | data['week'] = _trending_for_week(metric) 26 | data['month'] = _trending_for_month(metric) 27 | data['year'] = _trending_for_year(metric) 28 | 29 | return data 30 | 31 | def _trending_for_current_day(metric=None): 32 | date = datetime.date.today() 33 | unaggregated_values = MetricItem.objects.filter(metric=metric) 34 | aggregated_values = MetricDay.objects.filter(metric=metric, created=date) 35 | count = 0 36 | 37 | for u in unaggregated_values: 38 | count = count + u.num 39 | 40 | for a in aggregated_values: 41 | count = count + a.num 42 | 43 | return count 44 | 45 | def _trending_for_yesterday(metric=None): 46 | today = datetime.date.today() 47 | yesterday_date = today - datetime.timedelta(days=1) 48 | previous_week_date = today - datetime.timedelta(weeks=1) 49 | previous_month_date = get_previous_month(today) 50 | 51 | data = { 52 | 'yesterday': 0, 53 | 'previous_week': 0, 54 | 'previous_month': 0, 55 | } 56 | 57 | try: 58 | yesterday = MetricDay.objects.get(metric=metric, created=yesterday_date) 59 | data['yesterday'] = yesterday.num 60 | except ObjectDoesNotExist: 61 | pass 62 | 63 | try: 64 | previous_week = MetricDay.objects.get(metric=metric, created=previous_week_date) 65 | data['previous_week'] = previous_week.num 66 | except ObjectDoesNotExist: 67 | pass 68 | 69 | try: 70 | previous_month = MetricDay.objects.get(metric=metric, created=previous_month_date) 71 | data['previous_month'] = previous_month.num 72 | except ObjectDoesNotExist: 73 | pass 74 | 75 | return data 76 | 77 | def _trending_for_week(metric=None): 78 | this_week_date = week_for_date(datetime.date.today()) 79 | previous_week_date = this_week_date - datetime.timedelta(weeks=1) 80 | previous_month_week_date = get_previous_month(this_week_date) 81 | previous_year_week_date = get_previous_year(this_week_date) 82 | 83 | data = { 84 | 'week': 0, 85 | 'previous_week': 0, 86 | 'previous_month_week': 0, 87 | 'previous_year_week': 0, 88 | } 89 | 90 | try: 91 | week = MetricWeek.objects.get(metric=metric, created=this_week_date) 92 | data['week'] = week.num 93 | except ObjectDoesNotExist: 94 | pass 95 | 96 | try: 97 | previous_week = MetricWeek.objects.get(metric=metric, created=previous_week_date) 98 | data['previous_week'] = previous_week.num 99 | except ObjectDoesNotExist: 100 | pass 101 | 102 | try: 103 | previous_month_week = MetricWeek.objects.get(metric=metric, created=previous_month_week_date) 104 | data['previous_month_week'] = previous_month_week.num 105 | except ObjectDoesNotExist: 106 | pass 107 | 108 | try: 109 | previous_year_week = MetricWeek.objects.get(metric=metric, created=previous_year_week_date) 110 | data['previous_year_week'] = previous_year_week.num 111 | except ObjectDoesNotExist: 112 | pass 113 | 114 | return data 115 | 116 | def _trending_for_month(metric=None): 117 | this_month_date = month_for_date(datetime.date.today()) 118 | previous_month_date = get_previous_month(this_month_date) 119 | previous_month_year_date = get_previous_year(this_month_date) 120 | 121 | data = { 122 | 'month': 0, 123 | 'previous_month': 0, 124 | 'previous_month_year': 0 125 | } 126 | 127 | try: 128 | month = MetricMonth.objects.get(metric=metric, created=this_month_date) 129 | data['month'] = month.num 130 | except ObjectDoesNotExist: 131 | pass 132 | 133 | try: 134 | previous_month = MetricMonth.objects.get(metric=metric, created=previous_month_date) 135 | data['previous_month'] = previous_month.num 136 | except ObjectDoesNotExist: 137 | pass 138 | 139 | try: 140 | previous_month_year = MetricMonth.objects.get(metric=metric, created=previous_month_year_date) 141 | data['previous_month_year'] = previous_month_year.num 142 | except ObjectDoesNotExist: 143 | pass 144 | 145 | return data 146 | 147 | def _trending_for_year(metric=None): 148 | this_year_date = year_for_date(datetime.date.today()) 149 | previous_year_date = get_previous_year(this_year_date) 150 | 151 | data = { 152 | 'year': 0, 153 | 'previous_year': 0, 154 | } 155 | 156 | try: 157 | year = MetricYear.objects.get(metric=metric, created=this_year_date) 158 | data['year'] = year.num 159 | except ObjectDoesNotExist: 160 | pass 161 | 162 | try: 163 | previous_year = MetricYear.objects.get(metric=metric, created=previous_year_date) 164 | data['previous_year'] = previous_year.num 165 | except ObjectDoesNotExist: 166 | pass 167 | 168 | return data 169 | -------------------------------------------------------------------------------- /app_metrics/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import * 2 | 3 | from app_metrics.views import * 4 | 5 | urlpatterns = patterns('', 6 | url( 7 | regex = r'^reports/$', 8 | view = metric_report_view, 9 | name = 'app_metrics_reports', 10 | ), 11 | ) 12 | -------------------------------------------------------------------------------- /app_metrics/utils.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | import datetime 3 | import time 4 | from django.conf import settings 5 | from django.utils.importlib import import_module 6 | 7 | from app_metrics.exceptions import InvalidMetricsBackend, TimerError 8 | from app_metrics.models import Metric, MetricSet 9 | 10 | 11 | def collection_disabled(): 12 | return getattr(settings, 'APP_METRICS_DISABLED', False) 13 | 14 | 15 | def get_backend(): 16 | return getattr(settings, 'APP_METRICS_BACKEND', 'app_metrics.backends.db') 17 | 18 | 19 | def get_composite_backends(): 20 | return getattr(settings, 'APP_METRICS_COMPOSITE_BACKENDS', ()) 21 | 22 | 23 | def should_create_models(backend=None): 24 | if backend is None: 25 | backend = get_backend() 26 | 27 | if backend == 'app_metrics.backends.composite': 28 | backends = get_composite_backends() 29 | for b in backends: 30 | if b == 'app_metrics.backends.db': 31 | return True 32 | else: 33 | return backend == 'app_metrics.backends.db' 34 | 35 | 36 | def create_metric_set(name=None, metrics=None, email_recipients=None, 37 | no_email=False, send_daily=True, send_weekly=False, 38 | send_monthly=False): 39 | """ Create a metric set """ 40 | 41 | # This should be a NOOP for the non-database-backed backends 42 | if not should_create_models(): 43 | return 44 | 45 | try: 46 | metric_set = MetricSet( 47 | name=name, 48 | no_email=no_email, 49 | send_daily=send_daily, 50 | send_weekly=send_weekly, 51 | send_monthly=send_monthly) 52 | metric_set.save() 53 | 54 | for m in metrics: 55 | metric_set.metrics.add(m) 56 | 57 | for e in email_recipients: 58 | metric_set.email_recipients.add(e) 59 | 60 | except: 61 | return False 62 | 63 | return metric_set 64 | 65 | def create_metric(name, slug): 66 | """ Create a new type of metric to track """ 67 | 68 | # This should be a NOOP for the non-database-backed backends 69 | if not should_create_models(): 70 | return 71 | 72 | # See if this metric already exists 73 | existing = Metric.objects.filter(name=name, slug=slug) 74 | 75 | if existing: 76 | return False 77 | else: 78 | new_metric = Metric(name=name, slug=slug) 79 | new_metric.save() 80 | return new_metric 81 | 82 | def get_or_create_metric(name, slug): 83 | """ 84 | Returns the metric with the given name and slug, creating 85 | it if necessary 86 | """ 87 | 88 | # This should be a NOOP for the non-database-backed backends 89 | if not should_create_models(): 90 | return 91 | 92 | metric, created = Metric.objects.get_or_create(slug=slug, defaults={'name': name}) 93 | return metric 94 | 95 | 96 | def import_backend(): 97 | backend_string = get_backend() 98 | 99 | # Attempt to import the backend 100 | try: 101 | backend = import_module(backend_string) 102 | except Exception, e: 103 | raise InvalidMetricsBackend("Could not load '%s' as a backend: %s" % 104 | (backend_string, e)) 105 | 106 | return backend 107 | 108 | 109 | def metric(slug, num=1, **kwargs): 110 | """ Increment a metric """ 111 | if collection_disabled(): 112 | return 113 | 114 | backend = import_backend() 115 | 116 | try: 117 | backend.metric(slug, num, **kwargs) 118 | except Metric.DoesNotExist: 119 | create_metric(slug=slug, name='Autocreated Metric') 120 | 121 | 122 | class Timer(object): 123 | """ 124 | An object for manually controlling timing. Useful in situations where the 125 | ``timing`` context manager will not work. 126 | 127 | Usage:: 128 | 129 | timer = Timer() 130 | timer.start() 131 | 132 | # Do some stuff. 133 | 134 | timer.stop() 135 | 136 | # Returns a float of how many seconds the logic took. 137 | timer.elapsed() 138 | 139 | # Stores the float of how many seconds the logic took. 140 | timer.store() 141 | 142 | """ 143 | def __init__(self): 144 | self._start = None 145 | self._elapsed = None 146 | 147 | def timestamp(self): 148 | return time.time() 149 | 150 | def start(self): 151 | if self._start is not None: 152 | raise TimerError("You have already called '.start()' on this instance.") 153 | 154 | self._start = time.time() 155 | 156 | def stop(self): 157 | if self._start is None: 158 | raise TimerError("You must call '.start()' before calling '.stop()'.") 159 | 160 | self._elapsed = time.time() - self._start 161 | self._start = None 162 | 163 | def elapsed(self): 164 | if self._elapsed is None: 165 | raise TimerError("You must call '.stop()' before trying to get the elapsed time.") 166 | 167 | return self._elapsed 168 | 169 | def store(self, slug): 170 | if collection_disabled(): 171 | return 172 | backend = import_backend() 173 | backend.timing(slug, self.elapsed()) 174 | 175 | 176 | @contextmanager 177 | def timing(slug): 178 | """ 179 | A context manager to recording how long some logic takes & sends it off to 180 | the backend. 181 | 182 | Usage:: 183 | 184 | with timing('create_event'): 185 | # Your code here. 186 | # For example, create the event & all the related data. 187 | event = Event.objects.create( 188 | title='Coffee break', 189 | location='LPT', 190 | when=datetime.datetime(2012, 5, 4, 14, 0, 0) 191 | ) 192 | """ 193 | timer = Timer() 194 | timer.start() 195 | yield 196 | timer.stop() 197 | timer.store(slug) 198 | 199 | 200 | def gauge(slug, current_value, **kwargs): 201 | """Update a gauge.""" 202 | if collection_disabled(): 203 | return 204 | backend = import_backend() 205 | backend.gauge(slug, current_value, **kwargs) 206 | 207 | 208 | def week_for_date(date): 209 | return date - datetime.timedelta(days=date.weekday()) 210 | 211 | def month_for_date(month): 212 | return month - datetime.timedelta(days=month.day-1) 213 | 214 | def year_for_date(year): 215 | return datetime.date(year.year, 01, 01) 216 | 217 | def get_previous_month(date): 218 | if date.month == 1: 219 | month_change = 12 220 | else: 221 | month_change = date.month - 1 222 | new = date 223 | 224 | return new.replace(month=month_change) 225 | 226 | def get_previous_year(date): 227 | new = date 228 | return new.replace(year=new.year-1) 229 | 230 | -------------------------------------------------------------------------------- /app_metrics/views.py: -------------------------------------------------------------------------------- 1 | 2 | from django.contrib.auth.decorators import login_required 3 | from django.shortcuts import render_to_response 4 | from django.template import RequestContext 5 | 6 | @login_required 7 | def metric_report_view(request): 8 | return render_to_response('app_metrics/reports.html', {}, context_instance=RequestContext(request)) 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-app-metrics.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-app-metrics.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-app-metrics" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-app-metrics" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/backends.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Backends 3 | ======== 4 | 5 | .. attribute:: app_metrics.backends.db 6 | 7 | This backend stores all metrics and aggregations in your database. 8 | 9 | .. admonition:: NOTE 10 | 11 | Every call to metric() generates a database write, which may 12 | decrease your overall performance is you go nuts with them or have 13 | a heavily traffic site. 14 | 15 | .. _mixpanel_backend: 16 | 17 | .. attribute:: app_metrics.backends.mixpanel 18 | 19 | This backend allows you to pipe all of your calls to ``metric()`` to 20 | Mixpanel. See the `Mixpanel documentation`_ for more information on 21 | their API. 22 | 23 | .. _`Mixpanel documentation`: http://mixpanel.com/docs/api-documentation 24 | 25 | .. attribute:: app_metrics.backends.statsd 26 | 27 | This backend allows you to pipe all of your calls to ``metric()`` to a 28 | statsd server. See `statsd`_ for more information on their API. 29 | 30 | .. _`statsd`: https://github.com/etsy/statsd 31 | 32 | .. attribute:: app_metrics.backends.redis 33 | 34 | This backend allows you to use the metric() and gauge() aspects, but not 35 | timer aspects of app_metrics. 36 | 37 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | Version 0.8.0 6 | ------------- 7 | 8 | - Added Travis CI 9 | - Added librato backend 10 | - Added composite backends so you can send metrics to multiple backends automatically 11 | 12 | Previous Versions 13 | ----------------- 14 | 15 | Haven't really kept a strict history here, but we can recreate it from git logs. In short 16 | several contributors have added different backends such as statsd, redis, language translations, 17 | and docs. -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # django-app-metrics documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Nov 28 13:08:26 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | # extensions = [] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'django-app-metrics' 44 | copyright = u'2011, Frank Wiles' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.8.0' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.8.0' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 95 | if on_rtd: 96 | html_theme = 'default' 97 | else: 98 | html_theme = 'sphinxdoc' 99 | 100 | # Theme options are theme-specific and customize the look and feel of a theme 101 | # further. For a list of options available for each theme, see the 102 | # documentation. 103 | #html_theme_options = {} 104 | 105 | # Add any paths that contain custom themes here, relative to this directory. 106 | #html_theme_path = [] 107 | 108 | # The name for this set of Sphinx documents. If None, it defaults to 109 | # " v documentation". 110 | #html_title = None 111 | 112 | # A shorter title for the navigation bar. Default is the same as html_title. 113 | #html_short_title = None 114 | 115 | # The name of an image file (relative to this directory) to place at the top 116 | # of the sidebar. 117 | #html_logo = None 118 | 119 | # The name of an image file (within the static path) to use as favicon of the 120 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 121 | # pixels large. 122 | #html_favicon = None 123 | 124 | # Add any paths that contain custom static files (such as style sheets) here, 125 | # relative to this directory. They are copied after the builtin static files, 126 | # so a file named "default.css" will overwrite the builtin "default.css". 127 | html_static_path = ['_static'] 128 | 129 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 130 | # using the given strftime format. 131 | #html_last_updated_fmt = '%b %d, %Y' 132 | 133 | # If true, SmartyPants will be used to convert quotes and dashes to 134 | # typographically correct entities. 135 | #html_use_smartypants = True 136 | 137 | # Custom sidebar templates, maps document names to template names. 138 | #html_sidebars = {} 139 | 140 | # Additional templates that should be rendered to pages, maps page names to 141 | # template names. 142 | #html_additional_pages = {} 143 | 144 | # If false, no module index is generated. 145 | #html_domain_indices = True 146 | 147 | # If false, no index is generated. 148 | #html_use_index = True 149 | 150 | # If true, the index is split into individual pages for each letter. 151 | #html_split_index = False 152 | 153 | # If true, links to the reST sources are added to the pages. 154 | #html_show_sourcelink = True 155 | 156 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 157 | #html_show_sphinx = True 158 | 159 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 160 | #html_show_copyright = True 161 | 162 | # If true, an OpenSearch description file will be output, and all pages will 163 | # contain a tag referring to it. The value of this option must be the 164 | # base URL from which the finished HTML is served. 165 | #html_use_opensearch = '' 166 | 167 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 168 | #html_file_suffix = None 169 | 170 | # Output file base name for HTML help builder. 171 | htmlhelp_basename = 'django-app-metricsdoc' 172 | 173 | 174 | # -- Options for LaTeX output -------------------------------------------------- 175 | 176 | latex_elements = { 177 | # The paper size ('letterpaper' or 'a4paper'). 178 | #'papersize': 'letterpaper', 179 | 180 | # The font size ('10pt', '11pt' or '12pt'). 181 | #'pointsize': '10pt', 182 | 183 | # Additional stuff for the LaTeX preamble. 184 | #'preamble': '', 185 | } 186 | 187 | # Grouping the document tree into LaTeX files. List of tuples 188 | # (source start file, target name, title, author, documentclass [howto/manual]). 189 | latex_documents = [ 190 | ('index', 'django-app-metrics.tex', u'django-app-metrics Documentation', 191 | u'Frank Wiles', 'manual'), 192 | ] 193 | 194 | # The name of an image file (relative to this directory) to place at the top of 195 | # the title page. 196 | #latex_logo = None 197 | 198 | # For "manual" documents, if this is true, then toplevel headings are parts, 199 | # not chapters. 200 | #latex_use_parts = False 201 | 202 | # If true, show page references after internal links. 203 | #latex_show_pagerefs = False 204 | 205 | # If true, show URL addresses after external links. 206 | #latex_show_urls = False 207 | 208 | # Documents to append as an appendix to all manuals. 209 | #latex_appendices = [] 210 | 211 | # If false, no module index is generated. 212 | #latex_domain_indices = True 213 | 214 | 215 | # -- Options for manual page output -------------------------------------------- 216 | 217 | # One entry per manual page. List of tuples 218 | # (source start file, name, description, authors, manual section). 219 | man_pages = [ 220 | ('index', 'django-app-metrics', u'django-app-metrics Documentation', 221 | [u'Frank Wiles'], 1) 222 | ] 223 | 224 | # If true, show URL addresses after external links. 225 | #man_show_urls = False 226 | 227 | 228 | # -- Options for Texinfo output ------------------------------------------------ 229 | 230 | # Grouping the document tree into Texinfo files. List of tuples 231 | # (source start file, target name, title, author, 232 | # dir menu entry, description, category) 233 | texinfo_documents = [ 234 | ('index', 'django-app-metrics', u'django-app-metrics Documentation', 235 | u'Frank Wiles', 'django-app-metrics', 'One line description of project.', 236 | 'Miscellaneous'), 237 | ] 238 | 239 | # Documents to append as an appendix to all manuals. 240 | #texinfo_appendices = [] 241 | 242 | # If false, no module index is generated. 243 | #texinfo_domain_indices = True 244 | 245 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 246 | #texinfo_show_urls = 'footnote' 247 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. django-app-metrics documentation master file, created by 2 | sphinx-quickstart on Mon Nov 28 13:08:26 2011. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | ================== 7 | Django App Metrics 8 | ================== 9 | Django App Metrics allows you to capture and report on various events in your 10 | applications. 11 | 12 | Contents: 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | 17 | install 18 | usage 19 | settings 20 | backends 21 | changelog 22 | 23 | 24 | API: 25 | 26 | .. toctree:: 27 | :maxdepth: 2 28 | 29 | ref/utils 30 | 31 | Indices and tables 32 | ================== 33 | 34 | * :ref:`genindex` 35 | * :ref:`modindex` 36 | * :ref:`search` 37 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | Installing 6 | ========== 7 | 8 | * Install with pip:: 9 | 10 | pip install django-app-metrics 11 | 12 | * Add ``app_metrics`` to your ``INSTALLED_APPS`` setting:: 13 | 14 | INSTALLED_APPS = 15 | # ... 16 | 'app_metrics', 17 | ) 18 | 19 | * Edit :ref:`settings` in your project's settings module to your liking 20 | 21 | Requirements 22 | ============ 23 | celery and django-celery must be installed, however if you do not wish to 24 | actually use celery you can simply set CELERY_ALWAYS_EAGER = True in your 25 | settings and it will behave as if celery was not configured. 26 | 27 | Django 1.2 or higher required. 28 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-app-metrics.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-app-metrics.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/ref/utils.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Utility Functions 3 | ================= 4 | 5 | ``create_metric`` 6 | ================= 7 | 8 | .. function:: create_metric(name, slug) 9 | 10 | Creates a new type of metric track 11 | 12 | Arguments 13 | --------- 14 | 15 | ``name`` 16 | The verbose name of the metric to track 17 | 18 | ``slug`` 19 | The identifying slug used for the metric. This is what is passed into :func:`metric` to increment the metric 20 | 21 | 22 | ``metric`` 23 | ========== 24 | .. function:: metric(slug, num=1, \*\*kwargs) 25 | 26 | Increment a metric by ``num`` 27 | 28 | Shortcut to the current backend (as set by :attr:`APP_METRICS_BACKEND` metric method.) 29 | 30 | .. admonition:: Note 31 | 32 | If there is no metric mapped to ``slug``, a metric named ``Autocreated Metric`` with the passed in``slug`` will be auto-generated. 33 | 34 | Arguments 35 | --------- 36 | 37 | ``slug`` `(required)` 38 | Name of the metric to increment. 39 | 40 | ``num`` 41 | Number to increment the metric by. Defaults to 1. 42 | 43 | ``create_metric_set`` 44 | ===================== 45 | 46 | .. function:: create_metric_set(create_metric_set(name=None, metrics=None, email_recipients=None, no_email=False, send_daily=True, send_weekly=False, send_monthly=False) 47 | 48 | Creates a new metric set 49 | 50 | Arguments 51 | --------- 52 | 53 | ``name`` 54 | Verbose name given to the new metric_set 55 | 56 | ``metrics`` 57 | Iterable of slugs that the metric set should collect 58 | 59 | ``email_recipients`` 60 | Iterable of Users_ who should be emailed with updates on the metric set 61 | 62 | .. _Users: https://docs.djangoproject.com/en/1.3/topics/auth/#django.contrib.auth.models.User 63 | 64 | -------------------------------------------------------------------------------- /docs/settings.rst: -------------------------------------------------------------------------------- 1 | .. _settings: 2 | 3 | ======== 4 | Settings 5 | ======== 6 | 7 | 8 | Base Settings 9 | ============= 10 | .. attribute:: APP_METRICS_BACKEND 11 | 12 | Defaults to :attr:`app_metrics.backends.db` if not defined. 13 | 14 | Mixpanel Backend Settings 15 | ========================= 16 | These settings are only necessary if you're using the :ref:`Mixpanel backend` 17 | 18 | .. attribute:: APP_METRICS_MIXPANEL_TOKEN 19 | 20 | Your Mixpanel.com API token 21 | 22 | .. attribute:: APP_METERICS_MIXPANEL_URL 23 | 24 | Allow overriding of the API URL end point 25 | 26 | Statsd Settings 27 | =============== 28 | 29 | .. attribute:: APP_METRICS_STATSD_HOST 30 | 31 | Hostname of statsd server, defaults to 'localhost' 32 | 33 | .. attribute:: APP_METRICS_STATSD_PORT 34 | 35 | statsd port, defaults to '8125' 36 | 37 | .. attribute:: APP_METRICS_STATSD_SAMPLE_RATE 38 | 39 | statsd sample rate, defaults to 1 40 | 41 | Redis Settings 42 | ============== 43 | 44 | .. attribute:: APP_METRICS_REDIS_HOST 45 | 46 | Hostname of redis server, defaults to 'localhost' 47 | 48 | .. attribute:: APP_METRICS_REDIS_PORT 49 | 50 | redis port, defaults to '6379' 51 | 52 | .. attribute:: APP_METRICS_REDIS_DB 53 | 54 | redis database number to use, defaults to 0 55 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | Example Code 6 | ============ 7 | The utility functions ``create_metric`` and ``metric`` are the main API hooks to app_metrics. 8 | 9 | Example:: 10 | 11 | from django.contrib.auth.models import User 12 | 13 | from app_metrics.utils import create_metric, metric, gauge 14 | 15 | user1 = User.objects.get(pk='bob') 16 | user2 = User.objects.get(pk='jane') 17 | 18 | # Create a new metric to track 19 | my_metric = create_metric(name='New User Metric', slug='new_user_signup') 20 | 21 | # Create a MetricSet which ties a metric to an email schedule and sets 22 | # who should receive it 23 | 24 | my_metric_set = create_metric_set(name='My Set', 25 | metrics=[my_metric], 26 | email_recipients=[user1, user2]) 27 | 28 | # Increment the metric by one 29 | metric('new_user_signup') 30 | 31 | # Increment the metric by some other number 32 | metric('new_user_signup', 4) 33 | 34 | # Create a timer (only supported in statsd backend currently) 35 | with timing('mytimer'): 36 | for x in some_long_list: 37 | call_time_consuming_function(x) 38 | 39 | # Or if a context manager doesn't work for you you can use a Timer class 40 | t = Timer() 41 | t.start() 42 | something_that_takes_forever() 43 | t.stop() 44 | t.store('mytimer') 45 | 46 | # Gauges are current status type dials (think fuel gauge in a car) 47 | # These simply store and retrieve a value 48 | gauge('current_fuel', '30') 49 | gauge('load_load', '3.14') 50 | 51 | Management Commands 52 | =================== 53 | 54 | metrics_aggregate 55 | ----------------- 56 | 57 | Aggregate metric items into daily, weekly, monthly, and yearly totals 58 | It's fairly smart about it, so you're safe to run this as often as you 59 | like:: 60 | 61 | manage.py metrics_aggregate 62 | 63 | metrics_send_mail 64 | ----------------- 65 | 66 | Send email reports to users. The email will be sent out using django_mailer_'s ``send_htmlmailer`` if it is installed, otherwise defaults to django.core.mail_. Can be called like:: 67 | 68 | manage.py metrics_send_mail 69 | 70 | 71 | .. _django_mailer: https://github.com/jtauber/django-mailer/ 72 | .. _django.core.mail: https://docs.djangoproject.com/en/dev/topics/email/ 73 | -------------------------------------------------------------------------------- /requirements/tests.txt: -------------------------------------------------------------------------------- 1 | coverage==3.5.2 2 | django-coverage==1.2.2 3 | mock==0.8.0 4 | django-celery==3.0.6 5 | # python-statsd==1.5.4 6 | redis==2.6.2 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | from app_metrics import VERSION 5 | 6 | 7 | f = open(os.path.join(os.path.dirname(__file__), 'README.rst')) 8 | readme = f.read() 9 | f.close() 10 | 11 | setup( 12 | name='django-app-metrics', 13 | version=".".join(map(str, VERSION)), 14 | description='django-app-metrics is a reusable Django application for tracking and emailing application metrics.', 15 | long_description=readme, 16 | author='Frank Wiles', 17 | author_email='frank@revsys.com', 18 | url='https://github.com/frankwiles/django-app-metrics', 19 | packages=find_packages(), 20 | package_data={ 21 | 'app_metrics': [ 22 | 'templates/app_metrics/*', 23 | ] 24 | }, 25 | install_requires = [ 26 | 'celery', 27 | 'django-celery', 28 | ], 29 | tests_require = ['mock', 'django-coverage', 'coverage'], 30 | classifiers=[ 31 | 'Development Status :: 4 - Beta', 32 | 'Environment :: Web Environment', 33 | 'Intended Audience :: Developers', 34 | 'License :: OSI Approved :: BSD License', 35 | 'Operating System :: OS Independent', 36 | 'Programming Language :: Python', 37 | 'Framework :: Django', 38 | ], 39 | ) 40 | 41 | --------------------------------------------------------------------------------