import logging from django.db import models from bugsink.transaction import immediate_atomic from bugsink.app_settings import get_settings logger = logging.getLogger("bugsink.ingest") class StoreEnvelope: def __init__(self, ingested_at, project_pk, request): self._read = b"" self._ingested_at = ingested_at self._project_pk = project_pk self.request = request def read(self, size): result = self.request.read(size) if result: self._read += result return result def __getattr__(self, attr): return getattr(self.request, attr) # `immediate_atomic` here, rather than in the calling spot, to avoid its usage on the DontStoreEnvelope case. # Also: all the transaction stuff is kinda overkill anyway, for something that's completely unconnected to our real # data, i.e. can't really conflict... but in the sqlite world being explicit about where the transactions are is # always a good thing, i.e. keeps them small) @immediate_atomic() def store(self): # read the rest of the request; the regular .ingest() method breaks early by design self._read += self.request.read() if Envelope.objects.count() >= get_settings().KEEP_ENVELOPES: # >= b/c about to add # -1 because 0-indexed; we delete including the boundary, so we'll have space for the new one boundary = Envelope.objects.order_by("-ingested_at")[get_settings().KEEP_ENVELOPES - 1] Envelope.objects.filter(ingested_at__lte=boundary.ingested_at).delete() envelope = Envelope.objects.create( ingested_at=self._ingested_at, project_pk=self._project_pk, data=self._read, ) # arguably "debug", but if you turned StoreEnvelope on, you probably want to use its results "soon", and I'd # rather not have another thing for people to configure. logger.info("envelope stored, available at %s%s", get_settings().BASE_URL, envelope.get_absolute_url()) class DontStoreEnvelope: """conform to the same interface as StoreEnvelope, but don't store anything""" def __init__(self, request): self.request = request def __getattr__(self, attr): return getattr(self.request, attr) def store(self): pass class Envelope(models.Model): # id is implied which makes it an Integer. Great for sorting ingested_at = models.DateTimeField(blank=False, null=False) # we just use PK to avoid passing Projects around for debug code, and avoid FK-constraints too. project_pk = models.IntegerField(blank=False) # binary, because we don't want to make any assumptions about what we get "over the wire" (whether it's even utf-8) data = models.BinaryField(blank=False, null=False) class Meta: indexes = [ models.Index(fields=["ingested_at"]), ] def get_absolute_url(self): return f"/ingest/envelope/{self.pk}/"