Logging details available via Apprise/API/Website (#34)

This commit is contained in:
Chris Caron 2021-01-01 17:10:44 -05:00 committed by GitHub
parent b29ddf15c0
commit a57b621c29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 220 additions and 31 deletions

View File

@ -164,7 +164,7 @@ The use of environment variables allow you to provide over-rides to default sett
| `SECRET_KEY` | A Django variable acting as a *salt* for most things that require security. This API uses it for the hash sequences when writing the configuration files to disk (`hash` mode only). | `SECRET_KEY` | A Django variable acting as a *salt* for most things that require security. This API uses it for the hash sequences when writing the configuration files to disk (`hash` mode only).
| `ALLOWED_HOSTS` | A list of strings representing the host/domain names that this API can serve. This is a security measure to prevent HTTP Host header attacks, which are possible even under many seemingly-safe web server configurations. By default this is set to `*` allowing any host. Use space to delimit more than one host. | `ALLOWED_HOSTS` | A list of strings representing the host/domain names that this API can serve. This is a security measure to prevent HTTP Host header attacks, which are possible even under many seemingly-safe web server configurations. By default this is set to `*` allowing any host. Use space to delimit more than one host.
| `BASE_URL` | Those who are hosting the API behind a proxy that requires a subpath to gain access to this API should specify this path here as well. By default this is not set at all. | `BASE_URL` | Those who are hosting the API behind a proxy that requires a subpath to gain access to this API should specify this path here as well. By default this is not set at all.
| `LOG_LEVEL` | Adjust the log level to the console; the default value is `DEBUG` if the `DEBUG` is set below otherwise the default is `INFO`. Possible values are `ERROR`, `WARNING`, `INFO`, and `DEBUG`. | `LOG_LEVEL` | Adjust the log level to the console. Possible values are `CRITICAL`, `ERROR`, `WARNING`, `INFO`, and `DEBUG`.
| `DEBUG` | This defaults to `False` however can be set to `True` if defined with a non-zero value (such as `1`). | `DEBUG` | This defaults to `False` however can be set to `True` if defined with a non-zero value (such as `1`).

View File

@ -267,11 +267,18 @@ document.querySelector('#addconfig').onsubmit = function(event) {
'{% trans "Successfully saved the specified URL(s)." %}', '{% trans "Successfully saved the specified URL(s)." %}',
'success' 'success'
); );
} else if(response.status > 500) {
// Disk issue
Swal.fire(
'{% trans "Save" %}',
'{% trans "There was an issue writing the configuration to your filesystem. Check your file permissions and try again." %}',
'error'
);
} else { } else {
// user notification // user notification
Swal.fire( Swal.fire(
'{% trans "Save" %}', '{% trans "Save" %}',
'{% trans "Failed to save the specified URL(s)." %}', '{% trans "Failed to save the specified URL(s). Check your syntax and try again." %}',
'error' 'error'
); );
} }
@ -338,24 +345,37 @@ document.querySelector('#donotify').onsubmit = function(event) {
let response = fetch('{% url "notify" key %}', { let response = fetch('{% url "notify" key %}', {
method: 'POST', method: 'POST',
body: body, body: body,
headers: {
'Accept': 'text/html',
'X-Apprise-Log-Level': 'info'
}
}).then(function(response) { }).then(function(response) {
response.text().then(function (html) {
if(response.status == 200) if(response.status == 200)
{ {
// user notification // user notification
Swal.fire( Swal.fire(
'{% trans "Notification" %}', '{% trans "Notification" %}',
'{% trans "Successfully sent the notification(s)." %}', '{% trans "Successfully sent the notification(s)." %}' + html,
'success' 'success'
); );
} else if(response.status == 424) {
// user notification
Swal.fire(
'{% trans "Notification" %}',
'{% trans "One or more of the notification(s) were not sent." %}' + html,
'warning'
);
} else { } else {
// user notification // user notification
Swal.fire( Swal.fire(
'{% trans "Notification" %}', '{% trans "Notification" %}',
'{% trans "Failed to send the notification(s)." %}', '{% trans "Failed to send the notification(s)." %}' + html,
'error' 'error'
); );
} }
}); });
});
return false; return false;
} }
{% endblock %} {% endblock %}

View File

@ -241,7 +241,7 @@ class NotifyTests(SimpleTestCase):
'format': 'invalid' 'format': 'invalid'
} }
# Test referencing a key that doesn't exist # Test case with format set to invalid
response = self.client.post( response = self.client.post(
'/notify/{}'.format(key), '/notify/{}'.format(key),
data=json.dumps(json_data), data=json.dumps(json_data),
@ -261,7 +261,7 @@ class NotifyTests(SimpleTestCase):
'format': None, 'format': None,
} }
# Test referencing a key that doesn't exist # Test case with format changed
response = self.client.post( response = self.client.post(
'/notify/{}'.format(key), '/notify/{}'.format(key),
data=json.dumps(json_data), data=json.dumps(json_data),
@ -284,3 +284,61 @@ class NotifyTests(SimpleTestCase):
assert response.status_code == 200 assert response.status_code == 200
assert mock_notify.call_count == 1 assert mock_notify.call_count == 1
# Reset our count
mock_notify.reset_mock()
headers = {
'HTTP_X_APPRISE_LOG_LEVEL': 'debug',
'HTTP_ACCEPT': 'text/plain',
}
# Test referencing a key that doesn't exist
response = self.client.post(
'/notify/{}'.format(key),
data=json.dumps(json_data),
content_type='application/json',
**headers,
)
assert response.status_code == 200
assert mock_notify.call_count == 1
assert response['content-type'] == 'text/plain'
headers = {
'HTTP_X_APPRISE_LOG_LEVEL': 'debug',
'HTTP_ACCEPT': 'text/html',
}
mock_notify.reset_mock()
# Test referencing a key that doesn't exist
response = self.client.post(
'/notify/{}'.format(key),
data=json.dumps(json_data),
content_type='application/json',
**headers,
)
assert response.status_code == 200
assert mock_notify.call_count == 1
assert response['content-type'] == 'text/html'
headers = {
'HTTP_X_APPRISE_LOG_LEVEL': 'invalid',
'HTTP_ACCEPT': 'text/*',
}
mock_notify.reset_mock()
# Test referencing a key that doesn't exist
response = self.client.post(
'/notify/{}'.format(key),
data=json.dumps(json_data),
content_type='application/json',
**headers,
)
assert response.status_code == 200
assert mock_notify.call_count == 1
assert response['content-type'] == 'text/html'

View File

@ -27,6 +27,7 @@ from django.http import HttpResponse
from django.http import JsonResponse from django.http import JsonResponse
from django.views import View from django.views import View
from django.conf import settings from django.conf import settings
from django.utils.html import escape
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
from django.views.decorators.gzip import gzip_page from django.views.decorators.gzip import gzip_page
@ -422,6 +423,39 @@ class NotifyView(View):
# Add our configuration # Add our configuration
a_obj.add(ac_obj) a_obj.add(ac_obj)
# Our return content type can be controlled by the Accept keyword
# If it includes /* or /html somewhere then we return html, otherwise
# we return the logs as they're processed in their text format.
# The HTML response type has a bit of overhead where as it's not
# the case with text/plain
content_type = \
'text/html' if re.search(r'text\/(\*|html)',
request.headers.get('Accept', ''),
re.IGNORECASE) \
else 'text/plain'
# Acquire our log level from headers if defined, otherwise use
# the global one set in the settings
level = request.headers.get(
'X-Apprise-Log-Level',
settings.LOGGING['loggers']['apprise']['level']).upper()
# Initialize our response object
response = None
if level in ('CRITICAL', 'ERROR' 'WARNING', 'INFO', 'DEBUG'):
level = getattr(apprise.logging, level)
esc = '<!!-!ESC!-!!>'
fmt = '<li class="log_%(levelname)s">' \
'<div class="log_time">%(asctime)s</div>' \
'<div class="log_level">%(levelname)s</div>' \
f'<div class="log_msg">{esc}%(message)s{esc}</div></li>' \
if content_type == 'text/html' else \
settings.LOGGING['formatters']['standard']['format']
# Now specify our format (and over-ride the default):
with apprise.LogCapture(level=level, fmt=fmt) as logs:
# Perform our notification at this point # Perform our notification at this point
result = a_obj.notify( result = a_obj.notify(
content.get('body'), content.get('body'),
@ -430,17 +464,47 @@ class NotifyView(View):
tag=content.get('tag'), tag=content.get('tag'),
) )
if content_type == 'text/html':
# Iterate over our entries so that we can prepare to escape
# things to be presented as HTML
esc = re.escape(esc)
entries = re.findall(
r'(?P<head><li .+?){}(?P<to_escape>.*?)'
r'{}(?P<tail>.+li>$)(?=$|<li .+{})'.format(
esc, esc, esc), logs.getvalue(),
re.DOTALL)
# Wrap logs in `<ul>` tag and escape our message body:
response = '<ul class="logs">{}</ul>'.format(
''.join([e[0] + escape(e[1]) + e[2] for e in entries]))
else: # content_type == 'text/plain'
response = logs.getvalue()
else:
# Perform our notification at this point without logging
result = a_obj.notify(
content.get('body'),
title=content.get('title', ''),
notify_type=content.get('type', apprise.NotifyType.INFO),
tag=content.get('tag'),
)
if not result: if not result:
# If at least one notification couldn't be sent; change up # If at least one notification couldn't be sent; change up
# the response to a 424 error code # the response to a 424 error code
return HttpResponse( return HttpResponse(
response if response is not None else
_('One or more notification could not be sent.'), _('One or more notification could not be sent.'),
content_type=content_type,
status=ResponseCode.failed_dependency) status=ResponseCode.failed_dependency)
# Return our retrieved content # Return our retrieved content
return HttpResponse( return HttpResponse(
response if response is not None else
_('Notification(s) sent.'), _('Notification(s) sent.'),
status=ResponseCode.okay content_type=content_type,
status=ResponseCode.okay,
) )

View File

@ -41,7 +41,10 @@ SECRET_KEY = os.environ.get(
# #
# export DJANGO_SETTINGS_MODULE=core.settings.debug # export DJANGO_SETTINGS_MODULE=core.settings.debug
# ./manage.py runserver # ./manage.py runserver
DEBUG = bool(os.environ.get("DEBUG", False)) #
# Support 'yes', '1', 'true', 'enable', 'active', and +
DEBUG = os.environ.get("DEBUG", 'No')[0].lower() in (
'a', 'y', '1', 't', 'e', '+')
# allow all hosts by default otherwise read from the # allow all hosts by default otherwise read from the
# ALLOWED_HOSTS environment variable # ALLOWED_HOSTS environment variable

View File

@ -75,3 +75,47 @@ textarea {
background-color: inherit; background-color: inherit;
border: 1px solid #e4e4e4; border: 1px solid #e4e4e4;
} }
/* Notification Details */
ul.logs {
font-family: monospace, monospace;
height: 60%;
overflow: auto;
}
ul.logs li {
display: flex;
text-align: left;
width: 50em;
}
ul.logs li div.log_time {
font-weight: normal;
flex: 0 15em;
}
ul.logs li div.log_level {
font-weight: bold;
align: right;
flex: 0 5em;
}
ul.logs li div.log_msg {
flex: 1;
}
ul.logs li.log_INFO {
color: black;
}
ul.logs li.log_DEBUG {
color: #606060;
}
ul.logs li.log_WARNING {
color: orange;
}
ul.logs li.log_ERROR {
color: #8B0000;
}

View File

@ -1,2 +1,2 @@
django django
apprise >= 0.8.8 apprise >= 0.9.0