1
|
|
|
import re |
2
|
|
|
from datetime import datetime |
3
|
|
|
from datetime import timedelta |
4
|
|
|
|
5
|
|
|
import redis |
6
|
|
|
from django.conf import settings |
7
|
|
|
from django.core.exceptions import ValidationError |
8
|
|
|
from django.core.validators import MaxValueValidator |
9
|
|
|
from django.core.validators import MinValueValidator |
10
|
|
|
from django.db import models |
11
|
|
|
from django.utils.translation import ugettext_lazy as _ |
12
|
|
|
|
13
|
|
|
from .utils import PY3 |
14
|
|
|
from .utils import cached_property |
15
|
|
|
|
16
|
|
|
try: |
17
|
|
|
from django.utils.datastructures import SortedDict as OrderedDict |
18
|
|
|
except ImportError: |
19
|
|
|
from django.utils.datastructures import OrderedDict |
20
|
|
|
|
21
|
|
|
REDISBOARD_DETAIL_FILTERS = [re.compile(name) for name in getattr(settings, 'REDISBOARD_DETAIL_FILTERS', ( |
22
|
|
|
'aof_enabled', 'bgrewriteaof_in_progress', 'bgsave_in_progress', |
23
|
|
|
'changes_since_last_save', 'db.*', 'last_save_time', 'multiplexing_api', |
24
|
|
|
'total_commands_processed', 'total_connections_received', 'uptime_in_days', |
25
|
|
|
'uptime_in_seconds', 'vm_enabled', 'redis_version' |
26
|
|
|
))] |
27
|
|
|
REDISBOARD_DETAIL_TIMESTAMP_KEYS = getattr(settings, 'REDISBOARD_DETAIL_TIMESTAMP_KEYS', ( |
28
|
|
|
'last_save_time', |
29
|
|
|
)) |
30
|
|
|
REDISBOARD_DETAIL_SECONDS_KEYS = getattr(settings, 'REDISBOARD_DETAIL_SECONDS_KEYS', ( |
31
|
|
|
'uptime_in_seconds', |
32
|
|
|
)) |
33
|
|
|
|
34
|
|
|
REDISBOARD_SLOWLOG_LEN = getattr(settings, 'REDISBOARD_SLOWLOG_LEN', 10) |
35
|
|
|
|
36
|
|
|
REDISBOARD_SOCKET_TIMEOUT = getattr(settings, 'REDISBOARD_SOCKET_TIMEOUT', None) |
37
|
|
|
REDISBOARD_SOCKET_CONNECT_TIMEOUT = getattr(settings, 'REDISBOARD_SOCKET_CONNECT_TIMEOUT', None) |
38
|
|
|
REDISBOARD_SOCKET_KEEPALIVE = getattr(settings, 'REDISBOARD_SOCKET_KEEPALIVE', None) |
39
|
|
|
REDISBOARD_SOCKET_KEEPALIVE_OPTIONS = getattr(settings, 'REDISBOARD_SOCKET_KEEPALIVE_OPTIONS', None) |
40
|
|
|
|
41
|
|
|
|
42
|
|
|
def prettify(key, value): |
43
|
|
|
if key in REDISBOARD_DETAIL_SECONDS_KEYS: |
44
|
|
|
return key, timedelta(seconds=value) |
45
|
|
|
elif key in REDISBOARD_DETAIL_TIMESTAMP_KEYS: |
46
|
|
|
return key, datetime.fromtimestamp(value) |
47
|
|
|
else: |
48
|
|
|
return key, value |
49
|
|
|
|
50
|
|
|
|
51
|
|
|
class RedisServer(models.Model): |
52
|
|
|
class Meta: |
53
|
|
|
unique_together = ('hostname', 'port') |
54
|
|
|
verbose_name = _("Redis Server") |
55
|
|
|
verbose_name_plural = _("Redis Servers") |
56
|
|
|
permissions = ( |
57
|
|
|
("can_inspect", "Can inspect redis servers"), |
58
|
|
|
) |
59
|
|
|
|
60
|
|
|
label = models.CharField( |
61
|
|
|
_('Label'), |
62
|
|
|
max_length=50, |
63
|
|
|
blank=True, |
64
|
|
|
null=True, |
65
|
|
|
) |
66
|
|
|
|
67
|
|
|
hostname = models.CharField( |
68
|
|
|
_("Hostname"), |
69
|
|
|
max_length=250, |
70
|
|
|
help_text=_('This can also be the absolute path to a redis socket') |
71
|
|
|
) |
72
|
|
|
|
73
|
|
|
port = models.IntegerField(_("Port"), validators=[ |
74
|
|
|
MaxValueValidator(65535), MinValueValidator(1) |
75
|
|
|
], default=6379, blank=True, null=True) |
76
|
|
|
password = models.CharField(_("Password"), max_length=250, |
77
|
|
|
null=True, blank=True) |
78
|
|
|
|
79
|
|
|
sampling_threshold = models.IntegerField( |
80
|
|
|
_("Sampling threshold"), |
81
|
|
|
default=1000, |
82
|
|
|
help_text=_("Number of keys after which only a sample (of random keys) is shown on the inspect page.") |
83
|
|
|
) |
84
|
|
|
sampling_size = models.IntegerField( |
85
|
|
|
_("Sampling size"), |
86
|
|
|
default=200, |
87
|
|
|
help_text=_("Number of random keys shown when sampling is used. Note that each key translates to a RANDOMKEY call in redis.") |
88
|
|
|
) |
89
|
|
|
|
90
|
|
|
def clean(self): |
91
|
|
|
if not self.hostname.startswith('/') and not self.port: |
92
|
|
|
raise ValidationError(_('Please provide either a hostname AND a port or the path to a redis socket')) |
93
|
|
|
|
94
|
|
|
@cached_property |
95
|
|
|
def connection(self): |
96
|
|
|
if self.hostname.startswith('/'): |
97
|
|
|
unix_socket_path = self.hostname |
98
|
|
|
hostname = None |
99
|
|
|
else: |
100
|
|
|
hostname = self.hostname |
101
|
|
|
unix_socket_path = None |
102
|
|
|
return redis.Redis( |
103
|
|
|
host=hostname, |
104
|
|
|
port=self.port, |
105
|
|
|
password=self.password, |
106
|
|
|
unix_socket_path=unix_socket_path, |
107
|
|
|
socket_timeout=REDISBOARD_SOCKET_TIMEOUT, |
108
|
|
|
socket_connect_timeout=REDISBOARD_SOCKET_CONNECT_TIMEOUT, |
109
|
|
|
socket_keepalive=REDISBOARD_SOCKET_KEEPALIVE, |
110
|
|
|
socket_keepalive_options=REDISBOARD_SOCKET_KEEPALIVE_OPTIONS, |
111
|
|
|
) |
112
|
|
|
|
113
|
|
|
@connection.deleter |
114
|
|
|
def connection(self, value): |
115
|
|
|
value.connection_pool.disconnect() |
116
|
|
|
|
117
|
|
|
@cached_property |
118
|
|
|
def stats(self): |
119
|
|
|
try: |
120
|
|
|
conn = self.connection |
121
|
|
|
info = conn.info() |
122
|
|
|
slowlog = conn.slowlog_get() |
123
|
|
|
slowlog_len = conn.slowlog_len() |
124
|
|
|
return { |
125
|
|
|
'status': 'UP', |
126
|
|
|
'details': info, |
127
|
|
|
'memory': "%s (peak: %s)" % ( |
128
|
|
|
info['used_memory_human'], |
129
|
|
|
info.get('used_memory_peak_human', 'n/a') |
130
|
|
|
), |
131
|
|
|
'clients': info['connected_clients'], |
132
|
|
|
'brief_details': OrderedDict( |
133
|
|
|
prettify(k, v) |
134
|
|
|
for name in REDISBOARD_DETAIL_FILTERS |
135
|
|
|
for k, v in (info.items() if PY3 else info.iteritems()) |
136
|
|
|
if name.match(k) |
137
|
|
|
), |
138
|
|
|
'slowlog': slowlog, |
139
|
|
|
'slowlog_len': slowlog_len, |
140
|
|
|
} |
141
|
|
|
except redis.exceptions.ConnectionError: |
142
|
|
|
return { |
143
|
|
|
'status': 'DOWN', |
144
|
|
|
'clients': 'n/a', |
145
|
|
|
'memory': 'n/a', |
146
|
|
|
'details': {}, |
147
|
|
|
'brief_details': {}, |
148
|
|
|
'slowlog': [], |
149
|
|
|
'slowlog_len': 0, |
150
|
|
|
} |
151
|
|
|
except redis.exceptions.ResponseError as exc: |
152
|
|
|
return { |
153
|
|
|
'status': 'ERROR: %s' % exc.args, |
154
|
|
|
'clients': 'n/a', |
155
|
|
|
'memory': 'n/a', |
156
|
|
|
'details': {}, |
157
|
|
|
'brief_details': {}, |
158
|
|
|
'slowlog': [], |
159
|
|
|
'slowlog_len': 0, |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
def __unicode__(self): |
163
|
|
|
if self.label: |
164
|
|
|
label = '%s (%%s)' % self.label |
165
|
|
|
else: |
166
|
|
|
label = '%s' |
167
|
|
|
|
168
|
|
|
if self.port: |
169
|
|
|
label = label % ('%s:%s' % (self.hostname, self.port)) |
170
|
|
|
else: |
171
|
|
|
label = label % self.hostname |
172
|
|
|
|
173
|
|
|
return label |
174
|
|
|
|
175
|
|
|
def slowlog_len(self): |
176
|
|
|
try: |
177
|
|
|
return self.connection.slowlog_len() |
178
|
|
|
except redis.exceptions.ConnectionError: |
179
|
|
|
return 0 |
180
|
|
|
|
181
|
|
|
def slowlog_get(self, limit=REDISBOARD_SLOWLOG_LEN): |
182
|
|
|
try: |
183
|
|
|
for slowlog in self.connection.slowlog_get(REDISBOARD_SLOWLOG_LEN): |
184
|
|
|
yield dict( |
185
|
|
|
id=slowlog['id'], |
186
|
|
|
ts=datetime.fromtimestamp(slowlog['start_time']), |
187
|
|
|
duration=slowlog['duration'], |
188
|
|
|
command=slowlog['command'], |
189
|
|
|
) |
190
|
|
|
|
191
|
|
|
except redis.exceptions.ConnectionError: |
192
|
|
|
pass |
193
|
|
|
|