Skip to main content

A Django app for logging changes in model fields.

Project description

PyPI - Python Version PyPI - Version Ruff pre-commit.ci status https://codecov.io/gh/Nibblex/django-field-logger/graph/badge.svg?token=H1N619SS8P PyPI - License

Django Field Logger

A Django app for logging changes in model fields.

How to set up?

  1. Add fieldlogger to your INSTALLED_APPS

  2. Run python manage.py migrate to initialize the model

  3. Add FIELD_LOGGER_SETTINGS to your settings.py file.

FIELD_LOGGER_SETTINGS = {
    'ENCODER': 'path.to.your.json.Encoder', # (default: None)
    'DECODER': 'path.to.your.json.Decoder', # (default: None)
    'LOGGING_ENABLED': True, # (default: True)
    'FAIL_SILENTLY': True, # (default: True)
    'LOGGING_APPS': {
        'your_app': {
            'logging_enabled': True, # (default: True)
            'fail_silently': True, # (default: True)
            'models': {
                'YourModel': {
                    'logging_enabled': True, # (default: True)
                    'fail_silently': True, # (default: True)
                    'fields': ['field1', 'field2'], # (default: [])
                    'exclude_fields': ['field3', 'field4'], # (default: [])
                    'callbacks': [
                        lambda instance, fields, logs: print(instance, fields, logs),
                        'yourapp.app.callbacks.your_function_name'
                    ], # (default: [])
                },
            },
            'callbacks': [
                lambda instance, fields, logs: print(instance, fields, logs),
                'yourapp.app.callbacks.your_function_name'
            ], # (default: [])
        },
    },
    'CALLBACKS': [
        lambda instance, fields, logs: print(instance, fields, logs),
        'yourapp.app.callbacks.your_function_name'
    ], # (default: [])
}
  • ENCODER and DECODER are optional. If you want to encode/decode your model instance fields, you can specify your encoder/decoder classes here. Your encoder/decoder classes must be subclasses of json.JSONEncoder and json.JSONDecoder respectively.

  • LOGGING_ENABLED is optional. If you want to disable logging globally, you can set this to False.

  • FAIL_SILENTLY is optional. If it is set to False, exceptions will be raised if the callback function fails.

  • LOGGING_APPS apps to be logged.

    • models models to be logged.

      • fields is optional. If you want to log only specific fields, you can specify them here. If you want to log all fields, you can use __all__ as a value.

      • exclude_fields is optional. If fields is not specified, all fields in the model will be logged except the ones specified here.

        Only fields with their own database column are loggable: reverse relations and database-generated fields (GeneratedField) are always skipped. Many-to-many fields are supported through the m2m_changed signal (see Many-to-many fields).

      • callbacks is optional. If you want to add a callback function to be called after logging all models in all apps, you can add it here. Callback functions must be callable objects. You can optionally specify a callback function path in your configuration. The best practice is to place your callback function in yourapp/callbacks.py. Callback functions must have three parameters as follows:

        # callback as a named function
        def your_callback(instance, fields, logs):
            # your code here
        
        # callback as a lambda function
        lambda instance, fields, logs: # your code here
        • instance the model instance that is being logged.

        • fields list of fields that are being logged.

        • logs dict of logs that are being created. The key is the field name and the value is the FieldLog instance.

How it works?

  • Obtains the FIELD_LOGGER_SETTINGS from your respective settings file based on your environment.

  • Initializes LOGGING_APPS with the relative project paths of your models based on your configuration variable.

  • Binds to the pre_save and post_save signals of each loggable model, and to the m2m_changed signal of each loggable many-to-many field.

  • For each field specified in the configuration variable, creates a record in the FieldLog model for each instance update.

  • Fixture loading (loaddata) is a restore, not a change, so it is never logged.

Example

This section serves as a small example to demonstrate how to use this package.

Supposing you have this configuration in your settings.py file:

FIELD_LOGGER_SETTINGS = {
    'LOGGING_APPS': {
        'drivers': {
            'models': {
                'Driver': {
                    'fields': ['driver_name']
                },
            },
        },
    },
}

Supposing you have a model called Driver with fields called latest_speed, driver_name, driver_id:

from fieldlogger.models import FieldLog

driver = Driver.objects.last()
driver.latest_speed = 5
driver.save()  # fieldlogger won't create a record since 'latest_speed' was not among the loggable fields

driver.driver_name = 'John Doe'
driver.save()  # a record with this driver is created

driver.driver_name = 'Jane Doe'
driver.save()  # a record with this driver is created

instance_id = driver.id
app_label = driver._meta.app_label
model_name = driver._meta.model_name

log = FieldLog.objects.filter(instance_id=instance_id, app_label=app_label, model_name=model_name).last()
print(log.field, log.old_value, log.new_value)  # prints: driver_name John Doe Jane Doe

Callback example

Supposing you have this function in yourapp/callbacks.py which sets the extra_data field of the FieldLog model:

def set_extra_data_for_driver_name(instance, fields, logs):
    log = logs.get('driver_name')
    if log:
        log.extra_data = {
            'name_length': len(log.new_value)
        }
        log.save()

Then you can add this callback function to your configuration like this:

FIELD_LOGGER_SETTINGS = {
    'LOGGING_APPS': {
        'drivers': {
            'models': {
                'Driver': {
                    'fields': ['driver_name'],
                    'callbacks': [
                        'yourapp.callbacks.set_extra_data_for_driver_name'
                    ]
                },
            },
        },
    },
}

The model structure

This package provides you a django model which is called FieldLog; which tracks each change to a model instance specified in your configuration mapping. An example record is as follows:

{
    'id': 2,
    'app_label': 'drivers',
    'model_name': 'driver',
    'instance_id': 1,
    'field': 'driver_name',
    'timestamp': datetime.datetime(2024, 1, 16, 9, 1, 14, 619568, tzinfo=<UTC>), # set when the log is created
    'old_value': 'John Doe',
    'new_value': 'Jane Doe',
    'extra_data': {}, # this is a JSONField, you can store any extra data here using callbacks or by overriding it directly
    'created': False, # this is a boolean field, if it is True, it means that instance is a newly created instance
}

Additionally, FieldLog model provides the following properties:

  • model: returns the model class of the instance that is being logged.

  • instance: returns the instance that is being logged.

  • previous_log: returns the previous log of the same field of the same instance, if any.

Values are stored as JSON and converted back to Python objects when a log is loaded:

  • Binary values are stored base64-encoded, so any binary content is supported.

  • Foreign key values are resolved back to model instances lazily (no query until the value is accessed). If the related instance was deleted, an unsaved instance carrying only the primary key is returned, so reading old logs never fails.

  • Models with a composite primary key (Django >= 5.2) are not supported.

The FieldLoggerMixin

This package provides you a mixin class which is called FieldLoggerMixin. This mixin class provides you the following property:

  • fieldlog_set since the FieldLog model has not a direct relation to the model that is being logged, you can use this property to get the logs of the instance that is being logged.

    driver = Driver.objects.last()
    logs = driver.fieldlog_set.all()

The FieldLoggerManager

Django signals are not fired on bulk operations, so changes made through bulk_create and bulk_update are not logged by default. This package provides a manager called FieldLoggerManager that overrides both methods to log field changes as well:

from django.db import models

from fieldlogger.managers import FieldLoggerManager

class Driver(models.Model):
    # ...

    objects = FieldLoggerManager()

Both methods accept two extra keyword arguments:

  • log_fields set it to False to skip logging for that call (default: True).

  • run_callbacks set it to False to skip the configured callbacks for that call (default: True).

Driver.objects.bulk_create([Driver(driver_name='John Doe')])
Driver.objects.bulk_update(drivers, ['driver_name'], run_callbacks=False)

Many-to-many fields

Many-to-many changes do not go through save(), so they are logged from the m2m_changed signal instead. Any loggable many-to-many field gets one log per change, holding the sorted lists of related primary keys before and after:

driver.cars.add(car1, car2)
log = driver.fieldlog_set.get(field='cars')
print(log.old_value, log.new_value)  # prints: [] [1, 2]
  • add, remove, set and clear are logged, from both sides of the relation (car.drivers.add(driver) also logs the change on driver).

  • Changes made directly on an explicit through model (e.g. Membership.objects.create(...)) do not fire m2m_changed, so they are not logged; this mirrors Django’s own behavior.

License

Copyright (C) 2024 Sergio Rodríguez

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See the LICENSE file for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_field_logger-0.3.0.tar.gz (33.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_field_logger-0.3.0-py3-none-any.whl (31.9 kB view details)

Uploaded Python 3

File details

Details for the file django_field_logger-0.3.0.tar.gz.

File metadata

  • Download URL: django_field_logger-0.3.0.tar.gz
  • Upload date:
  • Size: 33.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for django_field_logger-0.3.0.tar.gz
Algorithm Hash digest
SHA256 ff341aaaa91023a8bb2ad7719cb6e2190ab0ed95297549c6170ed5e9526309d8
MD5 d1a0b1806f7e388ca3adbaa0b7c8b52a
BLAKE2b-256 594378334e5a96ff92e90042b9db15413f356f6b41e358dbd541c7df78d49467

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_field_logger-0.3.0.tar.gz:

Publisher: publish.yml on Nibblex/django-field-logger

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_field_logger-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_field_logger-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 29cc39d892e2960aa8b914e4f490708956d50c2cd25f5684a691c49c85f4065c
MD5 93317b8102f2c87e9c399aaab1cb32ad
BLAKE2b-256 cd3f61ae9ffd6f965cbc767299ed2c42783f6da754678997b32bdc2fc8d31aa0

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_field_logger-0.3.0-py3-none-any.whl:

Publisher: publish.yml on Nibblex/django-field-logger

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page