Skip to content

Commit c04d0d0

Browse files
committed
Initial work on new time-series storage
1 parent ad2d320 commit c04d0d0

File tree

4 files changed

+199
-0
lines changed

4 files changed

+199
-0
lines changed

src/sentry/tsdb/__init__.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""
2+
sentry.tsdb
3+
~~~~~~~~~~~
4+
5+
:copyright: (c) 2010-2013 by the Sentry Team, see AUTHORS for more details.
6+
:license: BSD, see LICENSE for more details.
7+
"""

src/sentry/tsdb/manager.py

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""
2+
sentry.tsdb.manager
3+
~~~~~~~~~~~~~~~~~~~
4+
5+
:copyright: (c) 2010-2013 by the Sentry Team, see AUTHORS for more details.
6+
:license: BSD, see LICENSE for more details.
7+
"""
8+
9+
from django.db.models import F
10+
from django.utils import timezone
11+
12+
from sentry.manager import BaseManager
13+
from sentry.utils.models import create_or_update
14+
15+
from .utils import Granularity
16+
17+
18+
class PointManager(BaseManager):
19+
def fetch(self, key, start_timestamp, end_timestamp):
20+
# TODO
21+
raise NotImplementedError
22+
23+
def incr(self, key, amount=1, timestamp=None):
24+
"""
25+
Increments all granularities for ``key`` by ``amount``.
26+
"""
27+
if timestamp is None:
28+
timestamp = timezone.now()
29+
for granularity, _ in Granularity.get_choices():
30+
epoch = Granularity.normalize_to_epoch(granularity, timestamp)
31+
32+
create_or_update(
33+
model=self.model,
34+
key=key,
35+
epoch=epoch,
36+
granularity=granularity,
37+
defaults={
38+
'value': F('value') + amount,
39+
}
40+
)
41+
42+
def trim(self):
43+
"""
44+
Called periodically to flush the out of bounds points from the
45+
database.
46+
"""
47+
now = timezone.now()
48+
for granularity, _ in Granularity.get_choices():
49+
min_value = Granularity.get_min_value(granularity, now)
50+
if min_value is None:
51+
continue
52+
53+
self.filter(
54+
granularity=granularity,
55+
value__lt=min_value,
56+
).delete()

src/sentry/tsdb/models.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""
2+
sentry.tsdb.models
3+
~~~~~~~~~~~~~~~~~~
4+
5+
Get a key:
6+
7+
>>> key = Key.objects.get_or_create(
8+
>>> name='events.group.{}'.format(group_id)
9+
>>> )
10+
11+
Increment the key for the current time:
12+
13+
>>> now = timezone.now()
14+
>>> Point.objects.incr(key, timestamp=now)
15+
16+
Periodically flush unused data:
17+
18+
>>> if random.random() > 0.9:
19+
>>> Point.objects.trim()
20+
21+
22+
:copyright: (c) 2010-2013 by the Sentry Team, see AUTHORS for more details.
23+
:license: BSD, see LICENSE for more details.
24+
"""
25+
26+
27+
from django.db import models
28+
29+
from sentry.manager import BaseManager
30+
31+
from .manager import PointManager
32+
from .utils import Granularity
33+
34+
35+
class Key(models.Model):
36+
name = models.CharField(max_length=1000, unique=True)
37+
label = models.CharField(max_length=1000, null=True)
38+
39+
objects = BaseManager(cache_fields=['name'])
40+
41+
42+
class Point(models.Model):
43+
key = models.ForeignKey(Key)
44+
value = models.PositiveIntegerField(default=0)
45+
epoch = models.PositiveIntegerField()
46+
granularity = models.PositiveIntegerField(choices=Granularity.get_choices())
47+
48+
objects = PointManager()
49+
50+
class Meta:
51+
index_together = (
52+
('key', 'granularity', 'value'),
53+
('granularity', 'value'),
54+
)

src/sentry/tsdb/utils.py

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""
2+
sentry.tsdb.utils
3+
~~~~~~~~~~~~~~~~~
4+
5+
:copyright: (c) 2010-2013 by the Sentry Team, see AUTHORS for more details.
6+
:license: BSD, see LICENSE for more details.
7+
"""
8+
9+
from datetime import timedelta
10+
11+
ONE_MINUTE = 60
12+
ONE_HOUR = ONE_MINUTE * 60
13+
ONE_DAY = ONE_HOUR * 24
14+
ONE_WEEK = ONE_DAY * 7
15+
16+
17+
class Granularity(object):
18+
SECONDS = 0
19+
MINUTES = 1
20+
HOURS = 2
21+
DAYS = 3
22+
WEEKS = 4
23+
MONTHS = 5
24+
YEARS = 6
25+
ALL_TIME = 7
26+
27+
@classmethod
28+
def get_choices(cls):
29+
if hasattr(cls, '__choice_cache'):
30+
return cls.__choice_cache
31+
32+
results = []
33+
for name in dir(cls):
34+
if name.startswith('_'):
35+
continue
36+
if not name.upper() == name:
37+
continue
38+
results.append((getattr(cls, name), name.replace('_', ' ').title()))
39+
cls.__choice_cache = results
40+
return results
41+
42+
@classmethod
43+
def normalize_to_epoch(cls, granularity, timestamp):
44+
timestamp = timestamp.replace(microsecond=0)
45+
if granularity == cls.ALL_TIME:
46+
return 0
47+
if granularity == cls.SECONDS:
48+
return timestamp
49+
timestamp = timestamp.replace(seconds=0)
50+
if granularity == cls.MINUTES:
51+
return timestamp
52+
timestamp = timestamp.replace(minutes=0)
53+
if granularity == cls.HOURS:
54+
return timestamp
55+
timestamp = timestamp.replace(hours=0)
56+
if granularity == cls.WEEKS:
57+
timestamp -= timedelta(days=timestamp.weekday())
58+
elif granularity in (cls.MONTHS, cls.YEARS):
59+
timestamp = timestamp.replace(day=1)
60+
elif granularity == cls.YEARS:
61+
timestamp = timestamp.replace(month=1)
62+
return int(timestamp.strftime('%s'))
63+
64+
@classmethod
65+
def get_min_timestamp(cls, granularity, timestamp):
66+
if granularity in (cls.ALL_TIME, cls.YEARS):
67+
return None
68+
69+
if granularity == cls.SECONDS:
70+
timestamp -= timedelta(minutes=1)
71+
elif granularity == cls.MINUTES:
72+
timestamp -= timedelta(hours=1)
73+
elif granularity == cls.HOURS:
74+
timestamp -= timedelta(days=1)
75+
elif granularity == cls.DAYS:
76+
# days are stored for a full month
77+
timestamp -= timedelta(months=1)
78+
elif granularity == cls.WEEKS:
79+
# weeks are stored for a full year
80+
timestamp -= timedelta(years=1)
81+
82+
return cls.normalize_to_epoch(timestamp, granularity)

0 commit comments

Comments
 (0)