Skip to content

Commit

Permalink
Merge pull request #1162 from openhealthcare/add-elcid-logging-into-opal
Browse files Browse the repository at this point in the history
Add elcid the elcid loggers into opal.
  • Loading branch information
davidmiller committed Jun 2, 2017
2 parents ea13447 + 3a19a3c commit 4c6b806
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 57 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,6 @@
### 0.8.2.2 (Minor Release)
Adds a custom email logger. This enables Django error emails which removing any confidential patient data

### 0.8.2.1 (Minor Release)
Adds in the karma config to the MANIFEST.in so that we include the karma configs on pip install.

Expand Down
5 changes: 5 additions & 0 deletions doc/docs/reference/loggers.md
@@ -0,0 +1,5 @@
# opal.core.log.ConfidentialEmailer

A confidential emailer that does not send the stack trace but provides the
file name and error line that the issue happened. Along with the host name and
user who had the issue.
1 change: 1 addition & 0 deletions doc/docs/reference/reference_guides.md
Expand Up @@ -21,6 +21,7 @@ The following reference guides are available:
[opal.core.patient_lists](patient_list.md)|Patient Lists - defining different types of list|
[opal.core.plugin](plugin.md)| Plugins - defining plugins to package reusable functionality
[opal.core.menus](core_menus.md)| Menus - declaring application menus
[opal.core.log](core_log.md)| Log Helpers - custom email error loggers

### Angular Services
|
Expand Down
2 changes: 1 addition & 1 deletion opal/_version.py
@@ -1,4 +1,4 @@
"""
Declare our current version string
"""
__version__ = '0.8.2.1'
__version__ = '0.8.2.2'
42 changes: 42 additions & 0 deletions opal/core/log.py
@@ -0,0 +1,42 @@
from django.utils.log import AdminEmailHandler
from django.conf import settings


class ConfidentialEmailer(AdminEmailHandler):
def __init__(self, *args, **kwargs):
super(ConfidentialEmailer, self).__init__(*args, **kwargs)
self.include_html = False

def get_brand_name(self):
return getattr(settings, "OPAL_BRAND_NAME", "Unnamed Opal app")

def format_subject(self, subject):
return "{} error".format(self.get_brand_name())

def emit(self, record):
record.msg = 'Potentially identifiable data suppressed'
record.args = []
detail = ""
if hasattr(record, "request") and record.request:
if record.request.user.is_authenticated():
user = record.request.user.username
else:
user = "anonymous"

m = "Request to host {0} on application {1} from user {2} with {3}"

detail = m.format(
record.request.META.get("HTTP_HOST"),
self.get_brand_name(),
user,
record.request.META.get("REQUEST_METHOD"),
)
record.request = None

record.exc_text = "Exception raised at {0}:{1}".format(
record.filename,
record.lineno
)

record.exc_text += "\n{}".format(detail)
return super(ConfidentialEmailer, self).emit(record)
92 changes: 92 additions & 0 deletions opal/tests/test_log.py
@@ -0,0 +1,92 @@
import logging
import mock
from django.test import override_settings
from django.conf import settings
from opal.core.test import OpalTestCase
from opal.core.log import ConfidentialEmailer


# we mock the stream handler because we don't want
# unnecessary logging critical statements when running tests
@override_settings(DEBUG=False, OPAL_BRAND_NAME="Amazing Opal App")
@mock.patch('logging.StreamHandler.emit')
@mock.patch('django.utils.log.AdminEmailHandler.send_mail')
class LogOutputTestCase(OpalTestCase):
def test_request_logging_critical(self, send_mail, stream_handler):
logger = logging.getLogger('django.request')
logger.error('confidential error')
self.assertTrue(send_mail.called)
expected_subject = "Amazing Opal App error"
expected_body = "Potentially identifiable data suppressed"
call_args = send_mail.call_args
self.assertEqual(expected_subject, call_args[0][0])
self.assertIn(expected_body, call_args[0][1])
self.assertEqual(call_args[1]["html_message"], None)

def test_request_logging_with_arguments(self, send_mail, stream_handler):
logger = logging.getLogger('django.request')
logger.error('%s error', "confidential")
self.assertTrue(send_mail.called)
expected_subject = "Amazing Opal App error"
expected_body = "Potentially identifiable data suppressed"
call_args = send_mail.call_args
self.assertEqual(expected_subject, call_args[0][0])
self.assertIn(expected_body, call_args[0][1])
self.assertEqual(call_args[1]["html_message"], None)

@mock.patch('opal.core.log.AdminEmailHandler.emit')
def test_record_formatting(self, emitter, send_mail, stream_handler):
emailer = ConfidentialEmailer()
record = mock.MagicMock()
record.exc_text = "confidential"
record.args = ["some_args"]
record.filename = "some_file.py"
record.lineno = 20
request = self.rf.get("/some/url")
request.user = self.user
request.session = {}
record.request = request
emailer.emit(record)
self.assertEqual(
emitter.call_args[0][0].exc_text,
"Exception raised at some_file.py:20\nRequest to host None on application Amazing Opal App from user testuser with GET"
)

@mock.patch('opal.core.log.AdminEmailHandler.emit')
def test_anonymous_user_record_formatting(self, emitter, send_mail, stream_handler):
emailer = ConfidentialEmailer()
record = mock.MagicMock()
record.exc_text = "confidential"
record.args = ["some_args"]
record.filename = "some_file.py"
record.lineno = 20
request = self.rf.get("/some/url")
request.user = self.user

request.session = {}
record.request = request
record.request.META = mock.MagicMock()
mock_meta_dict = dict(HTTP_HOST="somewhere", REQUEST_METHOD="GET")
record.request.META.get.side_effect = lambda x: mock_meta_dict[x]

with mock.patch.object(request.user, "is_authenticated") as is_authenticated:
is_authenticated.return_value = False
emailer.emit(record)
self.assertEqual(
emitter.call_args[0][0].exc_text,
"Exception raised at some_file.py:20\nRequest to host somewhere on application Amazing Opal App from user anonymous with GET"
)

def test_no_email_on_info(self, send_mail, stream_handler):
logger = logging.getLogger('django.request')
logger.info('%s error', "confidential")
self.assertFalse(send_mail.called)

def handle_brand_name(self, emitter, send_mail, stream_handler):
del settings.OPAL_BRAND_NAME
logger = logging.getLogger('django.request')
logger.error('confidential error')
self.assertTrue(send_mail.called)
expected_subject = "unamed opal app error"
call_args = send_mail.call_args
self.assertEqual(expected_subject, call_args[0][0])
144 changes: 88 additions & 56 deletions runtests.py
Expand Up @@ -10,64 +10,96 @@
os.path.realpath(os.path.dirname(__file__)), "opal"
)


settings.configure(DEBUG=True,
DATABASES={
'default': {
'ENGINE': 'django.db.backends.sqlite3',
}
},
PROJECT_PATH=PROJECT_PATH,
ROOT_URLCONF='opal.urls',
USE_TZ=True,
OPAL_EXTRA_APPLICATION='',
DATE_FORMAT='d/m/Y',
DATE_INPUT_FORMATS=['%d/%m/%Y'],
DATETIME_FORMAT='d/m/Y H:i:s',
DATETIME_INPUT_FORMATS=['%d/%m/%Y %H:%M:%S'],
STATIC_URL='/assets/',
COMPRESS_ROOT='/tmp/',
TIME_ZONE='UTC',
OPAL_BRAND_NAME = 'opal',
INTEGRATING=False,
DEFAULT_DOMAIN='localhost',
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'opal.middleware.AngularCSRFRename',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'opal.middleware.DjangoReversionWorkaround',
'reversion.middleware.RevisionMiddleware',
'axes.middleware.FailedLoginMiddleware',
),
INSTALLED_APPS=('django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.staticfiles',
'django.contrib.sessions',
'django.contrib.admin',
'reversion',
'compressor',
'axes',
'djcelery',
'opal',
'opal.core.search',
'opal.tests'
),
MIGRATION_MODULES={
'opal': 'opal.nomigrations'
},
TEMPLATE_LOADERS = (
('django.template.loaders.cached.Loader', (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)),
),
CELERY_ALWAYS_EAGER=True
test_settings_config = dict(
DEBUG=True,
DATABASES={
'default': {
'ENGINE': 'django.db.backends.sqlite3',
}
},
PROJECT_PATH=PROJECT_PATH,
ROOT_URLCONF='opal.urls',
USE_TZ=True,
OPAL_EXTRA_APPLICATION='',
DATE_FORMAT='d/m/Y',
DATE_INPUT_FORMATS=['%d/%m/%Y'],
DATETIME_FORMAT='d/m/Y H:i:s',
DATETIME_INPUT_FORMATS=['%d/%m/%Y %H:%M:%S'],
STATIC_URL='/assets/',
COMPRESS_ROOT='/tmp/',
TIME_ZONE='UTC',
OPAL_BRAND_NAME = 'opal',
INTEGRATING=False,
DEFAULT_DOMAIN='localhost',
MIDDLEWARE_CLASSES=(
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'opal.middleware.AngularCSRFRename',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'opal.middleware.DjangoReversionWorkaround',
'reversion.middleware.RevisionMiddleware',
'axes.middleware.FailedLoginMiddleware',
),
INSTALLED_APPS=(
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.staticfiles',
'django.contrib.sessions',
'django.contrib.admin',
'reversion',
'compressor',
'axes',
'djcelery',
'opal',
'opal.core.search',
'opal.tests'
),
MIGRATION_MODULES={
'opal': 'opal.nomigrations'
},
TEMPLATE_LOADERS = ((
'django.template.loaders.cached.Loader', (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)
),),
CELERY_ALWAYS_EAGER=True,
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse'
}
},
'handlers': {
'console': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'logging.StreamHandler'
},
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'opal.core.log.ConfidentialEmailer'
},
},
'loggers': {
'django.request': {
'handlers': ['console', 'mail_admins'],
'level': 'ERROR',
'propagate': True,
},
}
}
)

from opal.tests import dummy_opal_application

settings.configure(**test_settings_config)

from opal.tests import dummy_opal_application # NOQA


import django
Expand Down

0 comments on commit 4c6b806

Please sign in to comment.