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 LogEntry model for storing user action logs.

ActionAuditMixin is used to automate logging in django admin. For class based views, use BaseActionAuditMixin.

To manually log user action, use log_action() (or log_bulk_action() for bulk operations).

Tracker log entry

TrackerLogEntry is an extension to django’s 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 observation changes in review page.

Use track_change() to log the change into tracker.

Reversion

Reversion stores object’s changes over time in Revision and Version models.

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()

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 (document draft).

See Revision History for more details.

Analytics

Built-in analytics

Anonymised information about objects viewed by the user is stored in AnalyticsEntry model that can be accessed by clients and displayed in dashboard widgets.

Use AnalyticsViewLogMixin to log view to the analytics model automatically.

Google Analytics

Google analytics are automatically sent by the front-end based on the GOOGLE_ANALYTICS_4_PROPERTY_ID configuration.

See also

Google Analytics

Logging code

These utilities help monitor the system in deployment or when debugging locally.

Python logging

Python has built-in logging facility.

Logs are writen to a log file, and standard output, depending how project is configured. Configure LOGFILE_LOG_LEVEL and CONSOLE_LOG_LEVEL to defined which messages get logged where. Errors can be written to a separate error file, or e-mailed if LOG_ERRORS or EMAIL_ERRORS is set.

Utilities are provided by this project to simplify adding logs. Use logger.get_logger() to get logger instance, or logger.log_debug() shortcut to log a debug message.

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 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 logger.trace_span() (context manager) and logger.trace_decorator() (function / method decorator). Use them to monitor and troubleshoot performance of python functions.

See also

More about monitoring in Telemetry

Crash reports

Crashes from production are automatically sent to Sentry. Staging Sites send crash reports to GitLab Error Tracking

If you need to log an error manually, use sentry_sdk.capture_exception()

manually capture exception and report to Sentry without crashing
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 catch_exception() context manager.

Automatically capture exception and ignore it
with catch_exception(TypeError, KeyError):
    ...

Configuration errors

Invalid configuration of django models such as dashboard widgets, report rules, etc can cause crashes that will not be reported to 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 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 CONFIG_ERROR_REPORT_RATE_LIMIT_HOURS per each error.

See also

Task #29232 is where the functionality was originally implemented. It documents rationale and decisions behind this feature.

ConfigurationError is the base error class for configuration errors. This exception and it’s subclasses are handled by 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.

Example usage
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
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
try:
    ...
except ConfigurationError as e:
    handle_config_error(e)