===========================
Logging & error reports
===========================
Logging user actions
=======================
User actions within the system, such as viewing and modifying data, should leave an audit trace.
These logs are stored in the database.
Django action logs
-------------------
Django provides :class:`~django.contrib.admin.models.LogEntry` model for storing user action logs.
:class:`~action_audit.admin_mixin.ActionAuditMixin` is used to automate logging in django admin.
For class based views, use :class:`~action_audit.utils.BaseActionAuditMixin`.
To manually log user action, use :func:`~audit.utils.log_action`
(or :func:`~audit.utils.log_bulk_action` for bulk operations).
Tracker log entry
^^^^^^^^^^^^^^^^^^^
:class:`~megforms.models.TrackerLogEntry` is an extension to django's :class:`~django.contrib.admin.models.LogEntry` model
that stores additional details about the change made by the user, such as previous and new value, and name of the field
where the change was made.
This is commonly used to track :term:`observation` changes in review page.
Use :meth:`~megforms.models.TrackerLogEntryManager.track_change` to log the change into tracker.
Reversion
-----------
Reversion stores object's changes over time in :class:`~reversion.models.Revision` and :class:`~reversion.models.Version` models.
.. code-block:: python
:caption: example usage of reversion
import reversion
with reversion.create_revision():
reversion.set_user(request.user)
reversion.set_comment("Created revision 1")
# Save a new model instance.
obj = YourModel()
obj.name = "obj v1"
obj.save()
with reversion.create_revision():
reversion.set_user(request.user)
reversion.set_comment("Created revision 2")
# Update the model instance.
obj.name = "obj v2"
obj.save()
.. seealso::
| `django-reversion `_
| `django-reversion documentation `_
CKEditor Revision history
--------------------------
CKEditor revision history plugin integration is implemented in :task:`29912`.
It tracks changes to fields using CKEditor 5 where version control plugin is enabled (:term:`document draft`).
See :ref:`ckeditor revision history` for more details.
.. seealso::
| `Revision history overview `_
Analytics
==========
Built-in analytics
--------------------
Anonymised information about objects viewed by the user is stored in :class:`~analytics.models.AnalyticsEntry` model
that can be accessed by clients and displayed in dashboard widgets.
Use :class:`~analytics.views.mixin.AnalyticsViewLogMixin` to log view to the analytics model automatically.
Google Analytics
-----------------
Google analytics are automatically sent by the front-end based on the :envvar:`GOOGLE_ANALYTICS_4_PROPERTY_ID` configuration.
.. seealso:: :ref:`google analytics`
Logging code
=============
These utilities help monitor the system in deployment or when debugging locally.
.. _python logging:
Python logging
-----------------
Python has built-in `logging `_ facility.
Logs are writen to a log file, and standard output, depending how project is configured.
Configure :envvar:`LOGFILE_LOG_LEVEL` and :envvar:`CONSOLE_LOG_LEVEL` to defined which messages get logged where.
Errors can be written to a separate error file, or e-mailed if :envvar:`LOG_ERRORS` or :envvar:`EMAIL_ERRORS` is set.
Utilities are provided by this project to simplify adding logs.
Use :func:`logger.get_logger` to get logger instance, or :func:`logger.log_debug` shortcut to log a debug message.
.. code-block:: python
:caption: example use of logger
from logger import get_logger, log_debug
get_logger().debug("debug message")
log_debug("Same as above")
get_logger().info("example information")
get_logger().warning("a warning message")
try:
...
except Exception as e:
get_logger().error("An error has occurred", exc_info=e)
.. important::
Do not use :func:`print` in production code, always use logger
(or stdout/stderr in `django commands `_)
Tracing
---------
Tracing sends samples of code and their execution time to Azure Insights.
Tracing utilities include :func:`logger.trace_span` (context manager) and :func:`logger.trace_decorator` (function / method decorator).
Use them to monitor and troubleshoot performance of python functions.
.. seealso:: More about monitoring in :ref:`telemetry`
.. _Crash reports:
Crash reports
--------------
Crashes from production are automatically sent to `Sentry `_.
:ref:`Staging_sites` send crash reports to `GitLab Error Tracking `_
If you need to log an error manually, use :func:`sentry_sdk.capture_exception`
.. code-block:: python
:caption: manually capture exception and report to Sentry without crashing
:emphasize-lines: 4-5
try:
...
except Exception:
import sentry_sdk
sentry_sdk.capture_exception()
If you can ignore the exception but still want to report it to Sentry, use the :func:`~logger.catch_exception` context manager.
.. code-block:: python
:caption: Automatically capture exception and ignore it
with catch_exception(TypeError, KeyError):
...
Configuration errors
---------------------
Invalid configuration of :term:`django models ` such as dashboard widgets, report rules, etc can cause crashes that will not be reported to :ref:`Sentry `.
These are most often stored in a json field where database constraints and validation are limited.
Instead, when underlying cause of the crash is determined to be misconfiguration, the error will be reported to the e-mail address set in :envvar:`CONFIG_ERROR_REPORT_EMAIL`.
If the address is not set, the errors will not be reported.
As these errors can occur at high frequency under heavy traffic, the error reporting is limited by :envvar:`CONFIG_ERROR_REPORT_RATE_LIMIT_HOURS` per each error.
.. seealso:: :task:`29232` is where the functionality was originally implemented. It documents rationale and decisions behind this feature.
:exc:`~client_management.exceptions.ConfigurationError` is the base error class for configuration errors.
This exception and it's subclasses are handled by :func:`~client_management.error_handler.handle_config_error`.
The exception must at minimum have a clear error message, and reference to the model instance holding the erroneous configuration. It does not need to be translatable.
The ``url`` and ``config_field`` parameters are good to have to add context to the error report e-mail.
.. code-block:: python
:caption: Example usage
:emphasize-lines: 5, 7
try:
ward_id: int = obj.config['ward_id']
Ward.objects.get(pk=ward_id)
except KeyError as e:
raise ConfigurationError("'ward_id' configuration is missing", obj, url=request.path, config_field='ward_id') from e
except Ward.DoesNotExist as e:
raise ConfigurationError(f"Ward {ward_id} does not exist", obj, url=request.path, config_field='ward_id') from e
.. code-block:: python
:caption: If the code should continue running despite the error, you can catch it and pass it to the handler that will send the report e-mail
:emphasize-lines: 3-4
try:
...
except ConfigurationError as e:
handle_config_error(e)