Repository: django-extensions/django-extensions

Issue:
BaseCommand that logs errors
Hi there,

First of all thanks for writing this great app, I use this in every single one of my projects!

I wrote a small `BaseCommand` that logs errors to `django.commands` so it can be easily monitored and handled via django's logging mechanism. Do you think this is suitable to be included in `django-extensions`?

``` python
import sys

import django
from django.core.management.base import BaseCommand
from django.utils.log import getLogger

class LoggingBaseCommand(BaseCommand):
    # A subclass of BaseCommand that logs errors to django.commands

    def __init__(self):
        super(LoggingBaseCommand, self).__init__()
        self._logger = getLogger('django.commands')

    def run_from_argv(self, argv):
        try:
            super(LoggingBaseCommand, self).run_from_argv(argv)
        except Exception, e:
            self._logger.error(e,
                exc_info=sys.exc_info(),
                extra={
                    'status_code': 500,
                }
            )
            raise
```

Obviously, any inputs are welcome. Let me know if you think this is a good fit for `django-extensions`. If yes, I'll open a pull request.



Discussion:
Look very useful.

Why did you chose to use 'run_from_argv' instead of 'execute' (or 'handle') ?

Using 'execute' will also make this applicable for when executing management commands from within a Python context like Celery tasks.

Could you please make a pull request for this ?


Resolving patch:
diff --git a/django_extensions/management/base.py b/django_extensions/management/base.py
new file mode 100644
--- /dev/null
+++ b/django_extensions/management/base.py
@@ -0,0 +1,59 @@
+import sys
+
+import django
+from django.core.management.base import BaseCommand
+from django.utils.log import getLogger
+
+
+logger = getLogger('django.commands')
+
+
+class LoggingBaseCommand(BaseCommand):
+    """
+    A subclass of BaseCommand that logs run time errors to `django.commands`.
+    To use this, create a management command subclassing LoggingBaseCommand:
+
+        from django_extensions.management.base import LoggingBaseCommand
+
+        class Command(LoggingBaseCommand):
+            help = 'Test error'
+
+            def handle(self, *args, **options):
+                raise Exception
+
+    
+    And then define a logging handler in settings.py:
+
+        LOGGING = {
+            ... # Other stuff here
+            
+            'handlers': {
+                'mail_admins': {
+                    'level': 'ERROR',
+                    'filters': ['require_debug_false'],
+                    'class': 'django.utils.log.AdminEmailHandler'
+                },
+            },
+            'loggers': {
+                'django.commands': {
+                    'handlers': ['mail_admins'],
+                    'level': 'ERROR',
+                    'propagate': False,
+                },
+            }
+
+        }
+        
+    """
+
+    def execute(self, *args, **options):
+        try:
+            super(LoggingBaseCommand, self).execute(*args, **options)
+        except Exception, e:
+            logger.error(e,
+                exc_info=sys.exc_info(),
+                extra={
+                    'status_code': 500,
+                }
+            )
+            raise
\ No newline at end of file


Test patch:
diff --git a/django_extensions/tests/__init__.py b/django_extensions/tests/__init__.py
--- a/django_extensions/tests/__init__.py
+++ b/django_extensions/tests/__init__.py
@@ -5,6 +5,7 @@
 from django_extensions.tests.json_field import JsonFieldTest
 from django_extensions.tests.uuid_field import UUIDFieldTest
 from django_extensions.tests.fields import AutoSlugFieldTest
+from django_extensions.tests.management_command import CommandTest
 
 __test__ = {
     'UTILS_TRUNCATE_LETTERS_TESTS': UTILS_TRUNCATE_LETTERS_TESTS,
diff --git a/django_extensions/tests/management/__init__.py b/django_extensions/tests/management/__init__.py
new file mode 100644
diff --git a/django_extensions/tests/management/commands/__init__.py b/django_extensions/tests/management/commands/__init__.py
new file mode 100644
diff --git a/django_extensions/tests/management/commands/error_raising_command.py b/django_extensions/tests/management/commands/error_raising_command.py
new file mode 100644
--- /dev/null
+++ b/django_extensions/tests/management/commands/error_raising_command.py
@@ -0,0 +1,9 @@
+
+from django_extensions.management.base import LoggingBaseCommand
+
+
+class Command(LoggingBaseCommand):
+    help = 'Test error'
+
+    def handle(self, *args, **options):
+        print error
\ No newline at end of file
diff --git a/django_extensions/tests/management_command.py b/django_extensions/tests/management_command.py
new file mode 100644
--- /dev/null
+++ b/django_extensions/tests/management_command.py
@@ -0,0 +1,36 @@
+import logging
+
+from django.core.management import call_command
+from django.test import TestCase
+from django.utils.log import getLogger
+
+
+class MockLoggingHandler(logging.Handler):
+    """Mock logging handler to check for expected logs."""
+
+    def __init__(self, *args, **kwargs):
+        self.reset()
+        logging.Handler.__init__(self, *args, **kwargs)
+
+    def emit(self, record):
+        self.messages[record.levelname.lower()].append(record.getMessage())
+
+    def reset(self):
+        self.messages = {
+            'debug': [],
+            'info': [],
+            'warning': [],
+            'error': [],
+            'critical': [],
+        }
+
+
+class CommandTest(TestCase):
+
+    def test_error_logging(self):
+        # Ensure command errors are properly logged and reraised
+        from django_extensions.management.base import logger
+        logger.addHandler(MockLoggingHandler())
+        self.assertRaises(NameError, call_command, 'error_raising_command')
+        handler = logger.handlers[0]
+        self.assertEqual(len(handler.messages['error']), 1)
\ No newline at end of file