class SearchDocument(Model): project = models.ForeignKey(Project) group = models.ForeignKey(Group) total_events = BoundedPositiveIntegerField(default=1) status = BoundedPositiveIntegerField(default=0) date_added = models.DateTimeField(default=timezone.now) date_changed = models.DateTimeField(default=timezone.now) objects = SearchDocumentManager() class Meta: unique_together = (('project', 'group'), ) __repr__ = sane_repr('project_id', 'group_id')
class GroupTag(Model): """ Stores the total number of messages seen by a group matching the given filter. """ project = models.ForeignKey(Project, null=True) group = models.ForeignKey(Group) times_seen = BoundedPositiveIntegerField(default=0) key = models.CharField(max_length=MAX_TAG_KEY_LENGTH) value = models.CharField(max_length=MAX_TAG_VALUE_LENGTH) last_seen = models.DateTimeField(default=timezone.now, db_index=True, null=True) first_seen = models.DateTimeField(default=timezone.now, db_index=True, null=True) objects = BaseManager() class Meta: db_table = 'sentry_messagefiltervalue' unique_together = (('project', 'key', 'value', 'group'), ) __repr__ = sane_repr('project_id', 'group_id', 'key', 'value') def save(self, *args, **kwargs): if not self.first_seen: self.first_seen = self.last_seen super(GroupTag, self).save(*args, **kwargs)
class SearchToken(Model): document = models.ForeignKey(SearchDocument, related_name="token_set") field = models.CharField(max_length=64, default='text') token = models.CharField(max_length=128) times_seen = BoundedPositiveIntegerField(default=1) objects = BaseManager() class Meta: unique_together = (('document', 'field', 'token'), ) __repr__ = sane_repr('document_id', 'field', 'token')
class Activity(Model): COMMENT = 0 SET_RESOLVED = 1 SET_UNRESOLVED = 2 SET_MUTED = 3 SET_PUBLIC = 4 SET_PRIVATE = 5 SET_REGRESSION = 6 CREATE_ISSUE = 7 TYPE = ( # (TYPE, verb-slug) (COMMENT, 'comment'), (SET_RESOLVED, 'set_resolved'), (SET_UNRESOLVED, 'set_unresolved'), (SET_MUTED, 'set_muted'), (SET_PUBLIC, 'set_public'), (SET_PRIVATE, 'set_private'), (SET_REGRESSION, 'set_regression'), (CREATE_ISSUE, 'create_issue'), ) project = models.ForeignKey(Project) group = models.ForeignKey(Group, null=True) event = models.ForeignKey(Event, null=True) # index on (type, ident) type = BoundedPositiveIntegerField(choices=TYPE) ident = models.CharField(max_length=64, null=True) # if the user is not set, it's assumed to be the system user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True) datetime = models.DateTimeField(default=timezone.now) data = GzippedDictField(null=True) __repr__ = sane_repr('project_id', 'group_id', 'event_id', 'user_id', 'type', 'ident') def save(self, *args, **kwargs): created = bool(not self.id) super(Activity, self).save(*args, **kwargs) if not created: return # HACK: support Group.num_comments if self.type == Activity.COMMENT: self.group.update(num_comments=F('num_comments') + 1) if self.event: self.event.update(num_comments=F('num_comments') + 1)
class TagKey(Model): """ Stores references to available filters keys. """ project = models.ForeignKey(Project) key = models.CharField(max_length=32) values_seen = BoundedPositiveIntegerField(default=0) objects = TagKeyManager() class Meta: db_table = 'sentry_filterkey' unique_together = (('project', 'key'), ) __repr__ = sane_repr('project_id', 'key')
class GroupTagKey(Model): """ Stores a unique tag key name for a group. An example key might be "url" or "server_name". """ project = models.ForeignKey(Project, null=True) group = models.ForeignKey(Group) key = models.CharField(max_length=MAX_TAG_KEY_LENGTH) values_seen = BoundedPositiveIntegerField(default=0) objects = BaseManager() class Meta: unique_together = (('project', 'group', 'key'), ) __repr__ = sane_repr('project_id', 'group_id', 'key')
class TagKey(Model): """ Stores references to available filters keys. """ project = models.ForeignKey(Project) key = models.CharField(max_length=MAX_TAG_KEY_LENGTH) values_seen = BoundedPositiveIntegerField(default=0) label = models.CharField(max_length=64, null=True) objects = TagKeyManager() class Meta: db_table = 'sentry_filterkey' unique_together = (('project', 'key'), ) __repr__ = sane_repr('project_id', 'key') def get_label(self): return self.label or self.key.replace('_', ' ').title()
class ProjectCountByMinute(Model): """ Stores the total number of messages seen by a project at N minute intervals. e.g. if it happened at 08:34:55 the time would be normalized to 08:30:00 """ project = models.ForeignKey(Project, null=True) date = models.DateTimeField() # normalized to HH:MM:00 times_seen = BoundedPositiveIntegerField(default=0) time_spent_total = models.FloatField(default=0) time_spent_count = BoundedIntegerField(default=0) objects = BaseManager() class Meta: unique_together = (('project', 'date'), ) __repr__ = sane_repr('project_id', 'date')
class TagValue(Model): """ Stores references to available filters. """ project = models.ForeignKey(Project, null=True) key = models.CharField(max_length=MAX_TAG_KEY_LENGTH) value = models.CharField(max_length=MAX_TAG_VALUE_LENGTH) data = GzippedDictField(blank=True, null=True) times_seen = BoundedPositiveIntegerField(default=0) last_seen = models.DateTimeField( default=timezone.now, db_index=True, null=True) first_seen = models.DateTimeField( default=timezone.now, db_index=True, null=True) objects = BaseManager() class Meta: db_table = 'sentry_filtervalue' unique_together = (('project', 'key', 'value'),) __repr__ = sane_repr('project_id', 'key', 'value')
class Group(EventBase): """ Aggregated message which summarizes a set of Events. """ status = BoundedPositiveIntegerField(default=0, choices=STATUS_LEVELS, db_index=True) times_seen = BoundedPositiveIntegerField(default=1, db_index=True) last_seen = models.DateTimeField(default=timezone.now, db_index=True) first_seen = models.DateTimeField(default=timezone.now, db_index=True) resolved_at = models.DateTimeField(null=True, db_index=True) # active_at should be the same as first_seen by default active_at = models.DateTimeField(null=True, db_index=True) time_spent_total = models.FloatField(default=0) time_spent_count = BoundedIntegerField(default=0) score = BoundedIntegerField(default=0) is_public = models.NullBooleanField(default=False, null=True) objects = GroupManager() class Meta: unique_together = (('project', 'checksum'), ) verbose_name_plural = _('grouped messages') verbose_name = _('grouped message') permissions = (("can_view", "Can view"), ) db_table = 'sentry_groupedmessage' __repr__ = sane_repr('project_id', 'checksum') def __unicode__(self): return "(%s) %s" % (self.times_seen, self.error()) def save(self, *args, **kwargs): if not self.last_seen: self.last_seen = timezone.now() if not self.first_seen: self.first_seen = self.last_seen if not self.active_at: self.active_at = self.first_seen if self.message: # We limit what we store for the message body self.message = self.message.splitlines()[0][:255] super(Group, self).save(*args, **kwargs) def delete(self): model_list = (GroupTagKey, GroupTag, GroupCountByMinute, EventMapping, Event) for model in model_list: logging.info('Removing %r objects where group=%s', model, self.id) has_results = True while has_results: has_results = False for obj in model.objects.filter(group=self)[:1000]: obj.delete() has_results = True super(Group, self).delete() def get_absolute_url(self): return absolute_uri( reverse('sentry-group', args=[self.team.slug, self.project.slug, self.id])) @property def avg_time_spent(self): if not self.time_spent_count: return return float(self.time_spent_total) / self.time_spent_count def natural_key(self): return (self.project, self.logger, self.culprit, self.checksum) def is_over_resolve_age(self): resolve_age = self.project.get_option('sentry:resolve_age', None) if not resolve_age: return False return self.last_seen < timezone.now() - timedelta( hours=int(resolve_age)) def is_muted(self): return self.get_status() == STATUS_MUTED def is_resolved(self): return self.get_status() == STATUS_RESOLVED def get_status(self): if self.status == STATUS_UNRESOLVED and self.is_over_resolve_age(): return STATUS_RESOLVED return self.status def get_score(self): return int( math.log(self.times_seen) * 600 + float(time.mktime(self.last_seen.timetuple()))) def get_latest_event(self): if not hasattr(self, '_latest_event'): try: self._latest_event = self.event_set.order_by('-datetime')[0] except IndexError: self._latest_event = None return self._latest_event def get_version(self): if not self.data: return if 'version' not in self.data: return module = self.data.get('module', 'ver') return module, self.data['version'] def get_unique_tags(self, tag): return self.grouptag_set.filter( project=self.project, key=tag, ).values_list( 'value', 'times_seen', 'first_seen', 'last_seen', ).order_by('-times_seen') def get_tags(self): if not hasattr(self, '_tag_cache'): self._tag_cache = sorted([ t for t in self.grouptagkey_set.filter( project=self.project, ).values_list('key', flat=True) if not t.startswith('sentry:') ]) return self._tag_cache
class EventBase(Model): """ Abstract base class for both Event and Group. """ project = models.ForeignKey(Project, null=True) logger = models.CharField(max_length=64, blank=True, default='root', db_index=True) level = BoundedPositiveIntegerField(choices=LOG_LEVELS.items(), default=logging.ERROR, blank=True, db_index=True) message = models.TextField() culprit = models.CharField(max_length=MAX_CULPRIT_LENGTH, blank=True, null=True, db_column='view') checksum = models.CharField(max_length=32, db_index=True) data = GzippedDictField(blank=True, null=True) num_comments = BoundedPositiveIntegerField(default=0, null=True) platform = models.CharField(max_length=64, null=True) class Meta: abstract = True def save(self, *args, **kwargs): if len(self.logger) > 64: self.logger = self.logger[0:61] + u"..." super(EventBase, self).save(*args, **kwargs) def error(self): message = strip(self.message) if message: message = truncatechars(message, 100) else: message = '<unlabeled message>' return message error.short_description = _('error') def has_two_part_message(self): message = strip(self.message) return '\n' in message or len(message) > 100 def message_top(self): culprit = strip(self.culprit) if culprit: return culprit message = strip(self.message) if not strip(message): return '<unlabeled message>' return truncatechars(message.splitlines()[0], 100) @property def team(self): return self.project.team @property def user_ident(self): """ The identifier from a user is considered from several interfaces. In order: - User.id - User.email - User.username - Http.env.REMOTE_ADDR """ user_data = self.data.get('sentry.interfaces.User') if user_data: ident = user_data.get('id') if ident: return 'id:%s' % (ident, ) ident = user_data.get('email') if ident: return 'email:%s' % (ident, ) ident = user_data.get('username') if ident: return 'username:%s' % (ident, ) http_data = self.data.get('sentry.interfaces.Http') if http_data: if 'env' in http_data: ident = http_data['env'].get('REMOTE_ADDR') if ident: return 'ip:%s' % (ident, ) return None
class Project(Model): """ Projects are permission based namespaces which generally are the top level entry point for all data. A project may be owned by only a single team, and may or may not have an owner (which is thought of as a project creator). """ PLATFORM_CHOICES = tuple((p, PLATFORM_TITLES.get(p, p.title())) for p in PLATFORM_LIST) + (('other', 'Other'), ) slug = models.SlugField(null=True) name = models.CharField(max_length=200) owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="sentry_owned_project_set", null=True) team = models.ForeignKey(Team, null=True) public = models.BooleanField(default=False) date_added = models.DateTimeField(default=timezone.now) status = BoundedPositiveIntegerField(default=0, choices=( (STATUS_VISIBLE, _('Visible')), (STATUS_HIDDEN, _('Hidden')), ), db_index=True) platform = models.CharField(max_length=32, choices=PLATFORM_CHOICES, null=True) objects = ProjectManager(cache_fields=[ 'pk', 'slug', ]) class Meta: unique_together = (('team', 'slug'), ) __repr__ = sane_repr('team_id', 'slug', 'owner_id') def __unicode__(self): return u'%s (%s)' % (self.name, self.slug) def save(self, *args, **kwargs): if not self.slug: slugify_instance(self, self.name, team=self.team) super(Project, self).save(*args, **kwargs) def delete(self): # This handles cascades properly # TODO: this doesn't clean up the index for model in (TagKey, TagValue, GroupTagKey, GroupTag, GroupCountByMinute, ProjectCountByMinute, Activity, EventMapping, Event, Group): logging.info('Removing %r objects where project=%s', model, self.id) has_results = True while has_results: has_results = False for obj in model.objects.filter(project=self)[:1000]: obj.delete() has_results = True super(Project, self).delete() def get_absolute_url(self): return absolute_uri( reverse('sentry-stream', args=[self.team.slug, self.slug])) def merge_to(self, project): if not isinstance(project, Project): project = Project.objects.get_from_cache(pk=project) for group in Group.objects.filter(project=self): try: other = Group.objects.get( project=project, logger=group.logger, culprit=group.culprit, checksum=group.checksum, ) except Group.DoesNotExist: group.update(project=project) for model in (Event, GroupTag, GroupCountByMinute): model.objects.filter(project=self, group=group).update(project=project) else: Event.objects.filter(group=group).update(group=other) for obj in GroupTag.objects.filter(group=group): obj2, created = GroupTag.objects.get_or_create( project=project, group=group, key=obj.key, value=obj.value, defaults={'times_seen': obj.times_seen}) if not created: obj2.update(times_seen=F('times_seen') + obj.times_seen) for obj in GroupCountByMinute.objects.filter(group=group): obj2, created = GroupCountByMinute.objects.get_or_create( project=project, group=group, date=obj.date, defaults={ 'times_seen': obj.times_seen, 'time_spent_total': obj.time_spent_total, 'time_spent_count': obj.time_spent_count, }) if not created: obj2.update( times_seen=F('times_seen') + obj.times_seen, time_spent_total=F('time_spent_total') + obj.time_spent_total, time_spent_count=F('time_spent_count') + obj.time_spent_count, ) for fv in TagValue.objects.filter(project=self): TagValue.objects.get_or_create(project=project, key=fv.key, value=fv.value) fv.delete() self.delete() def is_default_project(self): return str(self.id) == str(settings.SENTRY_PROJECT) or str( self.slug) == str(settings.SENTRY_PROJECT) def get_tags(self): if not hasattr(self, '_tag_cache'): tags = ProjectOption.objects.get_value(self, 'tags', None) if tags is None: tags = TagKey.objects.all_keys(self) self._tag_cache = [t for t in tags if not t.startswith('sentry:')] return self._tag_cache # TODO: Make these a mixin def update_option(self, *args, **kwargs): return ProjectOption.objects.set_value(self, *args, **kwargs) def get_option(self, *args, **kwargs): return ProjectOption.objects.get_value(self, *args, **kwargs)
class Alert(Model): project = models.ForeignKey(Project) group = models.ForeignKey(Group, null=True) datetime = models.DateTimeField(default=timezone.now) message = models.TextField() data = GzippedDictField(null=True) related_groups = models.ManyToManyField(Group, through='sentry.AlertRelatedGroup', related_name='related_alerts') status = BoundedPositiveIntegerField(default=0, choices=( (STATUS_UNRESOLVED, _('Unresolved')), (STATUS_RESOLVED, _('Resolved')), ), db_index=True) __repr__ = sane_repr('project_id', 'group_id', 'datetime') # TODO: move classmethods to manager @classmethod def get_recent_for_project(cls, project_id): return cls.objects.filter( project=project_id, group_id__isnull=True, datetime__gte=timezone.now() - timedelta(minutes=60), status=STATUS_UNRESOLVED, ).order_by('-datetime') @classmethod def maybe_alert(cls, project_id, message, group_id=None): now = timezone.now() manager = cls.objects # We only create an alert based on: # - an alert for the project hasn't been created in the last 30 minutes # - an alert for the event hasn't been created in the last 60 minutes # TODO: there is a race condition if we're calling this function for the same project if manager.filter(project=project_id, datetime__gte=now - timedelta(minutes=60)).exists(): return if manager.filter(project=project_id, group=group_id, datetime__gte=now - timedelta(minutes=60)).exists(): return alert = manager.create( project_id=project_id, group_id=group_id, datetime=now, message=message, ) if not group_id and has_trending(): # Capture the top 5 trending events at the time of this error related_groups = Group.objects.get_accelerated( [project_id], minutes=MINUTE_NORMALIZATION)[:5] for group in related_groups: AlertRelatedGroup.objects.create( group=group, alert=alert, ) return alert @property def team(self): return self.project.team @property def is_resolved(self): return (self.status == STATUS_RESOLVED or self.datetime < timezone.now() - timedelta(minutes=60)) def get_absolute_url(self): return absolute_uri( reverse('sentry-alert-details', args=[self.team.slug, self.project.slug, self.id]))
class DummyModel(Model): foo = models.CharField(max_length=32) normint = BoundedIntegerField(null=True) bigint = BoundedBigIntegerField(null=True) posint = BoundedPositiveIntegerField(null=True)