diff --git a/README.md b/README.md index 021797b..20b6e2c 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,45 @@ The following architectures are supported: `amd64`, `arm/v7`, and `arm64`. The f ## API Details +### Health Checks + +You can perform status or health checks on your server configuration by accessing `/status`. + +| Path | Method | Description | +|------------- | ------ | ----------- | +| `/status` | GET | Simply returns a server status. The server http response code is a `200` if the server is working correcty and a `417` if there was an unexpected issue. You can set the `Accept` header to `application/json` or `text/plain` for different response outputs. + +Below is a sample of just a simple text response: +```bash +# Request a general text response +# Output will read `OK` if everything is fine, otherwise it will return +# one or more of the following separated by a comma: +# - ATTACH_PERMISSION_ISSUE: Can not write attachments (likely a permission issue) +# - CONFIG_PERMISSION_ISSUE: Can not write configuration (likely a permission issue) +curl -X GET http://localhost:8000/status +``` + +Below is a sample of a JSON response: +```bash +curl -X GET -H "Accept: application/json" http://localhost:8000/status +``` +The above output may look like this: +```json +{ + "config_lock": false, + "status": { + "can_write_config": true, + "can_write_attach": true, + "details": ["OK"] + } +} +``` + +- The `config_lock` always cross references if the `APPRISE_CONFIG_LOCK` is enabled or not. +- The `status.can_write_config` defines if the configuration directory is writable or not. If the environment variable `APPRISE_STATEFUL_MODE` is set to `disabled`, this value will always read `false` and it will not impact the `status.details` +- The `status.can_write_attach` defines if the attachment directory is writable or not. If the environment variable `APPRISE_ATTACH_SIZE` or `APPRISE_MAX_ATTACHMENTS` is set to `0` (zero) or lower, this value will always read `false` and it will not impact the `status.details`. +- The `status.details` identifies the overall status. If there is more then 1 issue to report here, they will all show in this list. In a working orderly environment, this will always be set to `OK` and the http response type will be `200`. + ### Stateless Solution Some people may wish to only have a sidecar solution that does require use of any persistent storage. The following API endpoint can be used to directly send a notification of your choice to any of the [supported services by Apprise](https://github.com/caronc/apprise/wiki) without any storage based requirements: diff --git a/apprise_api/api/templates/config.html b/apprise_api/api/templates/config.html index 1fd7f11..abe8a55 100644 --- a/apprise_api/api/templates/config.html +++ b/apprise_api/api/templates/config.html @@ -22,15 +22,15 @@
{{ key }}{% trans "Getting Started" %}
-
- {% blocktrans %}
- Here is where you can store your Apprise configuration associated with the key
{{key}}
.
- {% endblocktrans %}
- For some examples on how to build a development environment around this, click here.
+ {% trans "Verify your Apprise API Status:" %} click here
+
+ -
+ {% trans "Here is where you can store your Apprise configuration associated with the key:" %}
{{key}}
+ {% trans "For some examples on how to build a development environment around this:" %} click here
-
{% blocktrans %}
- In the future you can return to this configuration screen at any time by placing the following into your
- browser:
+ In the future you can return to this configuration screen at any time by placing the following into your browser:
{% endblocktrans %}
{{request.scheme}}://{{request.META.HTTP_HOST}}{{BASE_URL}}/cfg/{{key}}
diff --git a/apprise_api/api/urls.py b/apprise_api/api/urls.py
index aae908e..0cc8a08 100644
--- a/apprise_api/api/urls.py
+++ b/apprise_api/api/urls.py
@@ -29,6 +29,9 @@
re_path(
r'^$',
views.WelcomeView.as_view(), name='welcome'),
+ re_path(
+ r'^status/?$',
+ views.HealthCheckView.as_view(), name='health'),
re_path(
r'^details/?$',
views.DetailsView.as_view(), name='details'),
diff --git a/apprise_api/api/utils.py b/apprise_api/api/utils.py
index 32f86b4..8498c2a 100644
--- a/apprise_api/api/utils.py
+++ b/apprise_api/api/utils.py
@@ -229,6 +229,9 @@ def parse_attachments(attachment_payload, files_request):
attachment_payload = (attachment_payload, )
count += 1
+ if settings.APPRISE_ATTACH_SIZE <= 0:
+ raise ValueError("The attachment size is restricted to 0MB")
+
if settings.APPRISE_MAX_ATTACHMENTS <= 0 or \
(settings.APPRISE_MAX_ATTACHMENTS > 0 and
count > settings.APPRISE_MAX_ATTACHMENTS):
@@ -750,3 +753,65 @@ def send_webhook(payload):
logger.debug('Socket Exception: %s' % str(e))
return
+
+def healthcheck():
+ """
+ Runs a status check on the data and returns the statistics
+ """
+
+ # Some status variables we can flip
+ response = {
+ 'can_write_config': False,
+ 'can_write_attach': False,
+ 'details': [],
+ }
+
+ if ConfigCache.mode != AppriseStoreMode.DISABLED:
+ # Update our Configuration Check Block
+ path = os.path.join(ConfigCache.root, '.tmp_healthcheck')
+ try:
+ os.makedirs(path, exist_ok=True)
+
+ # Write a small file
+ with tempfile.TemporaryFile(mode='w+b', dir=path) as fp:
+ # Test writing 1 block
+ fp.write(b'.')
+ # Read it back
+ fp.seek(0)
+ fp.read(1) == b'.'
+ # Toggle our status
+ response['can_write_config'] = True
+
+ except OSError:
+ # We can take an early exit
+ response['details'].append('CONFIG_PERMISSION_ISSUE')
+
+ if settings.APPRISE_MAX_ATTACHMENTS > 0 and settings.APPRISE_ATTACH_SIZE > 0:
+ # Test our ability to access write attachments
+
+ # Update our Configuration Check Block
+ path = os.path.join(settings.APPRISE_ATTACH_DIR, '.tmp_healthcheck')
+ try:
+ os.makedirs(path, exist_ok=True)
+
+ # Write a small file
+ with tempfile.TemporaryFile(mode='w+b', dir=path) as fp:
+ # Test writing 1 block
+ fp.write(b'.')
+ # Read it back
+ fp.seek(0)
+ fp.read(1) == b'.'
+ # Toggle our status
+ response['can_write_attach'] = True
+
+ except OSError:
+ # We can take an early exit
+ response['details'].append('ATTACH_PERMISSION_ISSUE')
+
+
+ if not response['details']:
+ response['details'].append('OK')
+
+ return response
+
+
diff --git a/apprise_api/api/views.py b/apprise_api/api/views.py
index 3e569c2..5f1cbbd 100644
--- a/apprise_api/api/views.py
+++ b/apprise_api/api/views.py
@@ -40,6 +40,7 @@
from .utils import ConfigCache
from .utils import apply_global_filters
from .utils import send_webhook
+from .utils import healthcheck
from .forms import AddByUrlForm
from .forms import AddByConfigForm
from .forms import NotifyForm
@@ -116,6 +117,7 @@ class ResponseCode(object):
not_found = 404
method_not_allowed = 405
method_not_accepted = 406
+ expectation_failed = 417
failed_dependency = 424
fields_too_large = 431
internal_server_error = 500
@@ -136,6 +138,44 @@ def get(self, request):
})
+@method_decorator((gzip_page, never_cache), name='dispatch')
+class HealthCheckView(View):
+ """
+ A Django view used to return a simple healthcheck
+ """
+
+ def get(self, request):
+ """
+ Handle a GET request
+ """
+ # Detect the format our incoming payload
+ json_payload = \
+ MIME_IS_JSON.match(
+ request.content_type
+ if request.content_type
+ else request.headers.get(
+ 'content-type', '')) is not None
+
+ # Detect the format our response should be in
+ json_response = True if json_payload \
+ and ACCEPT_ALL.match(request.headers.get('accept', '')) else \
+ MIME_IS_JSON.match(request.headers.get('accept', '')) is not None
+
+ # Run our healthcheck
+ response = healthcheck()
+
+ # Prepare our response
+ status = ResponseCode.okay if 'OK' in response['details'] else ResponseCode.expectation_failed
+ if not json_response:
+ response = ','.join(response['details'])
+
+ return HttpResponse(response, status=status, content_type='text/plain') \
+ if not json_response else JsonResponse({
+ 'config_lock': settings.APPRISE_CONFIG_LOCK,
+ 'status': response,
+ }, encoder=JSONEncoder, safe=False, status=status)
+
+
@method_decorator((gzip_page, never_cache), name='dispatch')
class DetailsView(View):
"""