Add Possibility To Store Whole Stacktrace
Problem Statement
When debugging some event based on history, it would be often helpful to know from where the change has come from. This is particularly true in larger applications where it's not practical to add history messages to all events.
Describe the Solution You'd Like
To address this issue, I propose storing the whole stacktrace of the event. While this might needlessly enlarge the database, denormalizing it to a table of stacktraces with hashes should not be too demanding for not-so-frequently modified objects.
Benefits of the Feature
Storing whole stacktraces for each event can be beneficial for many users of django-simple-history
. It provides a more detailed understanding of the changes made to the application, which can be invaluable for debugging and troubleshooting purposes.
Implementation Details
To implement this feature, I have created a custom solution that involves creating a new model, Stacktrace
, to store unique stacktraces. The Stacktrace
model uses a hash to uniquely identify each stacktrace, which is then linked to the corresponding historical record using the HistoricalRecordStacktrace
model.
Here's an example of how the implementation works:
# signals.py
import traceback
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.db.utils import DatabaseError, IntegrityError
from simple_history.signals import post_create_historical_record
from .models import HistoricalRecordStacktrace, Stacktrace
def get_full_stacktrace():
"""Get the full stacktrace including non-blenderhub code"""
return "".join(traceback.format_stack())
def create_stacktrace_link(sender, **kwargs):
"""
Signal handler to create stacktrace link when historical record is created
"""
history_instance = kwargs["history_instance"]
# Get and store stacktrace
full_trace = get_full_stacktrace()
try:
stacktrace = Stacktrace.get_or_create_from_trace(full_trace)
# Create link to historical record
history_type = ContentType.objects.get_for_model(history_instance.__class__)
HistoricalRecordStacktrace.objects.get_or_create(
history_record_type=history_type,
history_record_id=history_instance.history_id,
stacktrace=stacktrace,
)
except (ObjectDoesNotExist, AttributeError, DatabaseError, IntegrityError):
# DatabaseError: general database errors
# IntegrityError: unique constraint violations
# ObjectDoesNotExist: ContentType doesn't exist
# AttributeError: history_instance doesn't have required attributes
pass
post_create_historical_record.connect(create_stacktrace_link)
# models.py
import hashlib
from django.db import models
class Stacktrace(models.Model):
"""Stores unique stacktraces"""
hash = models.CharField(max_length=64, unique=True, db_index=True)
trace = models.TextField() # Stores the blenderhub-only trace
first_seen = models.DateTimeField(auto_now_add=True)
last_seen = models.DateTimeField(auto_now=True)
count = models.PositiveIntegerField(default=1)
def __str__(self):
return f"Stacktrace {self.hash[:8]} (used {self.count} times)"
@classmethod
def get_or_create_from_trace(cls, trace):
"""Get or create a Stacktrace instance from a trace string"""
trace_hash = hashlib.sha256(trace.encode()).hexdigest()
stacktrace, created = cls.objects.get_or_create(
hash=trace_hash,
defaults={
"trace": trace,
},
)
if not created:
stacktrace.count += 1
stacktrace.save(update_fields=["count", "last_seen"])
return stacktrace
class HistoricalRecordStacktrace(models.Model):
"""Links historical records to their stacktraces"""
history_record_type = models.ForeignKey(
"contenttypes.ContentType", on_delete=models.CASCADE, related_name="+"
)
history_record_id = models.CharField(max_length=50)
stacktrace = models.ForeignKey(
Stacktrace, on_delete=models.CASCADE, related_name="historical_records"
)
created = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
models.Index(fields=["history_record_type", "history_record_id"]),
]
unique_together = ("history_record_type", "history_record_id", "stacktrace")
def __str__(self):
return f"Stacktrace link for {self.history_record_type} #{self.history_record_id}"
Admin and object_history.html Modifications
I have also modified the admin and object_history.html
to include the stacktrace link.
Should You Make the PR?
If there is demand for this functionality and the PR would be pulled, I am willing to spend some time to make the PR and implement it directly to django-simple-history
. However, if there is no demand for this feature, it may not be worth the effort.
To determine whether there is demand for this feature, I suggest creating a pull request and discussing it with the community. This will help to gauge interest and provide feedback on the implementation.
Conclusion
Q: What is the problem statement behind this feature request?
A: When debugging some event based on history, it would be often helpful to know from where the change has come from. This is particularly true in larger applications where it's not practical to add history messages to all events.
Q: How does the proposed solution address this issue?
A: The proposed solution involves storing the whole stacktrace of the event. While this might needlessly enlarge the database, denormalizing it to a table of stacktraces with hashes should not be too demanding for not-so-frequently modified objects.
Q: What are the benefits of this feature?
A: Storing whole stacktraces for each event can be beneficial for many users of django-simple-history
. It provides a more detailed understanding of the changes made to the application, which can be invaluable for debugging and troubleshooting purposes.
Q: How does the implementation work?
A: The implementation involves creating a new model, Stacktrace
, to store unique stacktraces. The Stacktrace
model uses a hash to uniquely identify each stacktrace, which is then linked to the corresponding historical record using the HistoricalRecordStacktrace
model.
Q: What modifications were made to the admin and object_history.html?
A: I have also modified the admin and object_history.html
to include the stacktrace link.
Q: Should I make the PR?
A: If there is demand for this functionality and the PR would be pulled, I am willing to spend some time to make the PR and implement it directly to django-simple-history
. However, if there is no demand for this feature, it may not be worth the effort.
Q: How can I determine whether there is demand for this feature?
A: You can create a pull request and discuss it with the community. This will help to gauge interest and provide feedback on the implementation.
Q: What are the potential drawbacks of this feature?
A: The potential drawbacks of this feature include:
- It may needlessly enlarge the database.
- It may not be suitable for applications with a high volume of events.
Q: How can I implement this feature in my own application?
A: You can implement this feature in your own application by following the implementation details outlined in the article.
Q: Are there any alternatives to this feature?
A: Yes, there are alternatives to this feature, such as:
- Using a third-party library to store and manage stacktraces.
- Implementing a custom solution using a different database schema.
Q: How can I provide feedback on this feature?
A: You can provide feedback on this feature by commenting on the pull request or discussing it with the community.
Q: What is the next step in implementing this feature?
A: The next step in implementing this feature is to create a pull request and discuss it with the community. This will help to gauge interest and provide feedback on the implementation.