diff --git a/dsmr_backend/apps.py b/dsmr_backend/apps.py index 7b6d09124..504dc66b3 100644 --- a/dsmr_backend/apps.py +++ b/dsmr_backend/apps.py @@ -52,3 +52,19 @@ def check_scheduled_processes(**kwargs): ) return issues + + +@receiver(request_status) +def postgresql_check_database_size(**kwargs): # pragma: nocover + import dsmr_backend.services.backend + + pretty_size, bytes_size = dsmr_backend.services.backend.postgresql_total_database_size() + + if bytes_size < settings.DSMRREADER_STATUS_WARN_OVER_EXCESSIVE_DATABASE_SIZE: + return + + return MonitoringStatusIssue( + __name__, + _('Database growing large: {}, consider data cleanup (if not already enabled)').format(pretty_size), + timezone.now() + ) diff --git a/dsmr_backend/services/backend.py b/dsmr_backend/services/backend.py index 7669900a1..f714ae37d 100644 --- a/dsmr_backend/services/backend.py +++ b/dsmr_backend/services/backend.py @@ -1,3 +1,4 @@ +import logging from distutils.version import StrictVersion import datetime @@ -7,6 +8,7 @@ from django.db.models import Q from django.utils import timezone from django.core.cache import cache +from django.db import connection from dsmr_backend import signals from dsmr_backend.dto import MonitoringStatusIssue @@ -16,6 +18,9 @@ from dsmr_weather.models.settings import WeatherSettings +logger = logging.getLogger('dsmrreader') + + def get_capabilities(capability=None): """ Returns the capabilities of the data tracked, such as whether the meter supports gas readings or @@ -24,7 +29,7 @@ def get_capabilities(capability=None): Optionally returns a single capability when requested. """ # Caching time should be limited, but enough to make it matter, as this call is used A LOT. - capabilities = cache.get('capabilities') + capabilities = cache.get(settings.DSMRREADER_CAPABILITIES_CACHE) if capabilities is None: capabilities = { @@ -66,7 +71,7 @@ def get_capabilities(capability=None): capabilities['electricity_returned'] = False capabilities['any'] = any(capabilities.values()) - cache.set('capabilities', capabilities) + cache.set(settings.DSMRREADER_CAPABILITIES_CACHE, capabilities) # Single selection. if capability is not None: @@ -94,6 +99,16 @@ def is_timestamp_passed(timestamp): return timezone.now() >= timestamp +def request_cached_monitoring_status(): + cached_monitoring_status = cache.get(settings.DSMRREADER_MONITORING_CACHE) + + if cached_monitoring_status is None: + # This will also update the cache. + return request_monitoring_status() + + return cached_monitoring_status # pragma: nocover + + def request_monitoring_status(): """ Requests all apps to report any issues for monitoring. """ responses = signals.request_status.send_robust(None) @@ -103,6 +118,10 @@ def request_monitoring_status(): if not current_response: continue + if isinstance(current_response, Exception): + logger.warning(current_response) + continue + if not isinstance(current_response, (list, tuple)): current_response = [current_response] @@ -112,6 +131,9 @@ def request_monitoring_status(): issues = sorted(issues, key=lambda x: x.since, reverse=True) + # Always invalidate and update cache + cache.set(settings.DSMRREADER_MONITORING_CACHE, issues) + return issues @@ -139,3 +161,19 @@ def hours_in_day(day): # Unchanged else: return 24 + + +def postgresql_total_database_size(): # pragma: nocover + if connection.vendor != 'postgresql': + return + + with connection.cursor() as cursor: + database_name = settings.DATABASES['default']['NAME'] + size_sql = """ + SELECT pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname)) as pretty_size, + pg_catalog.pg_database_size(d.datname) as bytes_size + FROM pg_catalog.pg_database d + WHERE d.datname = %s; + """ + cursor.execute(size_sql, [database_name]) + return cursor.fetchone() diff --git a/dsmr_datalogger/apps.py b/dsmr_datalogger/apps.py index a1ff11a42..cda572281 100644 --- a/dsmr_datalogger/apps.py +++ b/dsmr_datalogger/apps.py @@ -38,3 +38,19 @@ def check_recent_readings(**kwargs): _('No recent readings received'), latest_reading.timestamp ) + + +@receiver(request_status) +def check_reading_count(**kwargs): # pragma: nocover + import dsmr_datalogger.services.datalogger + + reading_count = dsmr_datalogger.services.datalogger.postgresql_approximate_reading_count() + + if reading_count < settings.DSMRREADER_STATUS_WARN_OVER_EXCESSIVE_READING_COUNT: + return + + return MonitoringStatusIssue( + __name__, + _('Approximately {} readings stored, consider data cleanup (if not already enabled)').format(reading_count), + timezone.now() + ) diff --git a/dsmr_datalogger/services/datalogger.py b/dsmr_datalogger/services/datalogger.py index 77ce57e07..f61813d4c 100644 --- a/dsmr_datalogger/services/datalogger.py +++ b/dsmr_datalogger/services/datalogger.py @@ -2,6 +2,7 @@ from django.db.models.expressions import F from django.utils import timezone +from django.db import connection import serial from dsmr_datalogger.models.reading import DsmrReading @@ -206,3 +207,17 @@ def _get_dsmrreader_mapping(version): }) return mapping + + +def postgresql_approximate_reading_count(): # pragma: nocover + if connection.vendor != 'postgresql': + return + + # A live count is too slow on huge datasets, this is accurate enough: + with connection.cursor() as cursor: + cursor.execute( + 'SELECT reltuples as approximate_row_count FROM pg_class WHERE relname = %s;', + [DsmrReading._meta.db_table] + ) + reading_count = cursor.fetchone()[0] + return int(reading_count) diff --git a/dsmr_frontend/context_processors/__init__.py b/dsmr_frontend/context_processors/__init__.py index 547ca2205..4162fd94f 100644 --- a/dsmr_frontend/context_processors/__init__.py +++ b/dsmr_frontend/context_processors/__init__.py @@ -1,9 +1,12 @@ from django.conf import settings +import dsmr_backend.services.backend + def version(request): return { 'dsmr_version': settings.DSMRREADER_VERSION, + 'request_cached_monitoring_status': dsmr_backend.services.backend.request_cached_monitoring_status(), 'DSMRREADER_MAIN_BRANCH': settings.DSMRREADER_MAIN_BRANCH, 'LANGUAGE_CODE': request.LANGUAGE_CODE, } diff --git a/dsmr_frontend/templates/dsmr_frontend/base.html b/dsmr_frontend/templates/dsmr_frontend/base.html index ed311cf98..e02c79ff6 100644 --- a/dsmr_frontend/templates/dsmr_frontend/base.html +++ b/dsmr_frontend/templates/dsmr_frontend/base.html @@ -103,7 +103,14 @@