smartd_log improvements
lgz committed
Nov 29, 2017 at 19:04 UTC
1ef00cd9f49410a69b9a6b6cfe59378bb9a34d42
1 file changed
+299
-129
python.d/smartd_log.chart.py
+299
-129
@@ -3,174 +3,344 @@
3
# Author: l2isbad, vorph1
4
5
import os
6
-from re import compile as r_compile
6
+import re
7
+
8
+from collections import namedtuple
9
+from time import time
10
11
from bases.collection import read_last_line
12
from bases.FrameworkServices.SimpleService import SimpleService
13
14
# charts order (can be overridden if you want less charts, or different order)
12
-ORDER = ['1', '4', '5', '7', '9', '12', '193', '194', '197', '198', '200']
15
+# ORDER = ['1', '4', '5', '7', '9', '12', '193', '194', '197', '198', '200']
16
+ORDER = ['194']
17
18
SMART_ATTR = {
15
- '1': 'Read Error Rate',
16
- '2': 'Throughput Performance',
17
- '3': 'Spin-Up Time',
18
- '4': 'Start/Stop Count',
19
- '5': 'Reallocated Sectors Count',
20
- '6': 'Read Channel Margin',
21
- '7': 'Seek Error Rate',
22
- '8': 'Seek Time Performance',
23
- '9': 'Power-On Hours Count',
24
- '10': 'Spin-up Retries',
25
- '11': 'Calibration Retries',
26
- '12': 'Power Cycle Count',
27
- '13': 'Soft Read Error Rate',
28
- '100': 'Erase/Program Cycles',
29
- '103': 'Translation Table Rebuild',
30
- '108': 'Unknown (108)',
31
- '170': 'Reserved Block Count',
32
- '171': 'Program Fail Count',
33
- '172': 'Erase Fail Count',
34
- '173': 'Wear Leveller Worst Case Erase Count',
35
- '174': 'Unexpected Power Loss',
36
- '175': 'Program Fail Count',
37
- '176': 'Erase Fail Count',
38
- '177': 'Wear Leveling Count',
39
- '178': 'Used Reserved Block Count',
40
- '179': 'Used Reserved Block Count',
41
- '180': 'Unused Reserved Block Count',
42
- '181': 'Program Fail Count',
43
- '182': 'Erase Fail Count',
44
- '183': 'SATA Downshifts',
45
- '184': 'End-to-End error',
46
- '185': 'Head Stability',
47
- '186': 'Induced Op-Vibration Detection',
48
- '187': 'Reported Uncorrectable Errors',
49
- '188': 'Command Timeout',
50
- '189': 'High Fly Writes',
51
- '190': 'Temperature',
52
- '191': 'G-Sense Errors',
53
- '192': 'Power-Off Retract Cycles',
54
- '193': 'Load/Unload Cycles',
55
- '194': 'Temperature',
56
- '195': 'Hardware ECC Recovered',
57
- '196': 'Reallocation Events',
58
- '197': 'Current Pending Sectors',
59
- '198': 'Off-line Uncorrectable',
60
- '199': 'UDMA CRC Error Rate',
61
- '200': 'Write Error Rate',
62
- '201': 'Soft Read Errors',
63
- '202': 'Data Address Mark Errors',
64
- '203': 'Run Out Cancel',
65
- '204': 'Soft ECC Corrections',
66
- '205': 'Thermal Asperity Rate',
67
- '206': 'Flying Height',
68
- '207': 'Spin High Current',
69
- '209': 'Offline Seek Performance',
70
- '220': 'Disk Shift',
71
- '221': 'G-Sense Error Rate',
72
- '222': 'Loaded Hours',
73
- '223': 'Load/Unload Retries',
74
- '224': 'Load Friction',
75
- '225': 'Load/Unload Cycles',
76
- '226': 'Load-in Time',
77
- '227': 'Torque Amplification Count',
78
- '228': 'Power-Off Retracts',
79
- '230': 'GMR Head Amplitude',
80
- '231': 'Temperature',
81
- '232': 'Available Reserved Space',
82
- '233': 'Media Wearout Indicator',
83
- '240': 'Head Flying Hours',
84
- '241': 'Total LBAs Written',
85
- '242': 'Total LBAs Read',
86
- '250': 'Read Error Retry Rate'
19
+ '1': 'Read Error Rate',
20
+ '2': 'Throughput Performance',
21
+ '3': 'Spin-Up Time',
22
+ '4': 'Start/Stop Count',
23
+ '5': 'Reallocated Sectors Count',
24
+ '6': 'Read Channel Margin',
25
+ '7': 'Seek Error Rate',
26
+ '8': 'Seek Time Performance',
27
+ '9': 'Power-On Hours Count',
28
+ '10': 'Spin-up Retries',
29
+ '11': 'Calibration Retries',
30
+ '12': 'Power Cycle Count',
31
+ '13': 'Soft Read Error Rate',
32
+ '100': 'Erase/Program Cycles',
33
+ '103': 'Translation Table Rebuild',
34
+ '108': 'Unknown (108)',
35
+ '170': 'Reserved Block Count',
36
+ '171': 'Program Fail Count',
37
+ '172': 'Erase Fail Count',
38
+ '173': 'Wear Leveller Worst Case Erase Count',
39
+ '174': 'Unexpected Power Loss',
40
+ '175': 'Program Fail Count',
41
+ '176': 'Erase Fail Count',
42
+ '177': 'Wear Leveling Count',
43
+ '178': 'Used Reserved Block Count',
44
+ '179': 'Used Reserved Block Count',
45
+ '180': 'Unused Reserved Block Count',
46
+ '181': 'Program Fail Count',
47
+ '182': 'Erase Fail Count',
48
+ '183': 'SATA Downshifts',
49
+ '184': 'End-to-End error',
50
+ '185': 'Head Stability',
51
+ '186': 'Induced Op-Vibration Detection',
52
+ '187': 'Reported Uncorrectable Errors',
53
+ '188': 'Command Timeout',
54
+ '189': 'High Fly Writes',
55
+ '190': 'Temperature',
56
+ '191': 'G-Sense Errors',
57
+ '192': 'Power-Off Retract Cycles',
58
+ '193': 'Load/Unload Cycles',
59
+ '194': 'Temperature',
60
+ '195': 'Hardware ECC Recovered',
61
+ '196': 'Reallocation Events',
62
+ '197': 'Current Pending Sectors',
63
+ '198': 'Off-line Uncorrectable',
64
+ '199': 'UDMA CRC Error Rate',
65
+ '200': 'Write Error Rate',
66
+ '201': 'Soft Read Errors',
67
+ '202': 'Data Address Mark Errors',
68
+ '203': 'Run Out Cancel',
69
+ '204': 'Soft ECC Corrections',
70
+ '205': 'Thermal Asperity Rate',
71
+ '206': 'Flying Height',
72
+ '207': 'Spin High Current',
73
+ '209': 'Offline Seek Performance',
74
+ '220': 'Disk Shift',
75
+ '221': 'G-Sense Error Rate',
76
+ '222': 'Loaded Hours',
77
+ '223': 'Load/Unload Retries',
78
+ '224': 'Load Friction',
79
+ '225': 'Load/Unload Cycles',
80
+ '226': 'Load-in Time',
81
+ '227': 'Torque Amplification Count',
82
+ '228': 'Power-Off Retracts',
83
+ '230': 'GMR Head Amplitude',
84
+ '231': 'Temperature',
85
+ '232': 'Available Reserved Space',
86
+ '233': 'Media Wearout Indicator',
87
+ '240': 'Head Flying Hours',
88
+ '241': 'Total LBAs Written',
89
+ '242': 'Total LBAs Read',
90
+ '250': 'Read Error Retry Rate'
91
}
92
93
+LIMIT = namedtuple('LIMIT', ['min', 'max'])
94
90
-class Disk:
91
- def __init__(self, name, path):
92
- self.name = name
95
+LIMITS = {
96
+ '194': LIMIT(0, 200)
97
+}
98
+
99
+RESCAN_INTERVAL = 60
100
+
101
+REGEX = re.compile(
102
+ '(\d+);' # attribute
103
+ '(\d+);' # normalized value
104
+ '(\d+)', # raw value
105
+ re.X
106
+)
107
+
108
+
109
+def chart_template(attr, raw):
110
+ chart_name = 'attr_id' + attr
111
+ title = SMART_ATTR[attr]
112
+ units = 'raw' if raw else 'normalized'
113
+
114
+ return {
115
+ chart_name: {
116
+ 'options': [None, title, units, title.lower(), 'smartd_log.' + chart_name, 'line'],
117
+ 'lines': []
118
+ }
119
+ }
120
+
121
+
122
+def handle_os_error(method):
123
+ def on_call(*args):
124
+ try:
125
+ return method(*args)
126
+ except OSError:
127
+ return None
128
+ return on_call
129
+
130
+
131
+class SmartAttribute(object):
132
+ def __init__(self, idx, normalized, raw):
133
+ self.id = idx
134
+ self.normalized = normalized
135
+ self._raw = raw
136
+
137
+ @property
138
+ def raw(self):
139
+ if self.id in LIMITS:
140
+ limit = LIMITS[self.id]
141
+ if limit.min <= int(self._raw) <= limit.max:
142
+ return self._raw
143
+ return None
144
+ return self._raw
145
+
146
+ @raw.setter
147
+ def raw(self, value):
148
+ self._raw = value
149
+
150
+
151
+class DiskLogFile:
152
+ def __init__(self, path):
153
self.path = path
154
+ self.size = os.path.getsize(path)
155
+
156
+ @handle_os_error
157
+ def is_changed(self):
158
+ new_size = os.path.getsize(self.path)
159
+ old_size, self.size = self.size, new_size
160
+
161
+ return new_size != old_size and new_size
162
+
163
+ @staticmethod
164
+ @handle_os_error
165
+ def is_valid(log_file, exclude):
166
+ return all([log_file.endswith('.csv'),
167
+ not [p for p in exclude if p in log_file],
168
+ os.access(log_file, os.R_OK),
169
+ os.path.getsize(log_file)])
170
+
171
+
172
+class Disk:
173
+ def __init__(self, full_path, age):
174
+ self.log_file = DiskLogFile(full_path)
175
+ self.name = os.path.basename(full_path).split('.')[-3]
176
+ self.age = int(age)
177
self.status = True
178
+ self.attributes = dict()
179
+
180
+ self.get_attributes()
181
+
182
+ def __eq__(self, other):
183
+ if isinstance(other, Disk):
184
+ return self.name == other.name
185
+ return self.name == other
186
+
187
+ @handle_os_error
188
+ def is_active(self):
189
+ return (time() - os.path.getmtime(self.log_file.path)) / 60 < self.age
190
+
191
+ @handle_os_error
192
+ def get_attributes(self):
193
+ last_line = read_last_line(self.log_file.path)
194
+ self.attributes = dict((attr, SmartAttribute(attr, normalized, raw)) for attr, normalized, raw
195
+ in REGEX.findall(last_line))
196
+ return True
197
+
198
+ def data(self, raw=None):
199
+ data = dict()
200
+ for attr in self.attributes.values():
201
+ value = attr.raw if raw else attr.normalized
202
+ if value is None:
203
+ continue
204
+ key = '_'.join([self.name, attr.id])
205
+ data[key] = value
206
+ return data
207
208
209
class Service(SimpleService):
210
def __init__(self, configuration=None, name=None):
211
SimpleService.__init__(self, configuration=configuration, name=name)
100
- self.regex = r_compile(r'(\d+);(\d+);(\d+)')
212
self.log_path = self.configuration.get('log_path', '/var/log/smartd')
102
- self.raw_values = self.configuration.get('raw_values')
103
- self.attr = self.configuration.get('smart_attributes', [])
104
- self.exclude_disks = self.configuration.get('exclude_disks', str()).split()
213
+ self.raw = self.configuration.get('raw_values', True)
214
+ self.exclude = self.configuration.get('exclude_disks', str()).split()
215
+ self.age = self.configuration.get('age', 60)
216
+
217
+ self.runs = 0
218
+ self.disks = list()
219
self.order = list()
220
self.definitions = dict()
107
- self.disks = list()
108
-
109
- for path_to_disk in find_disks_in_log_path(self.log_path):
110
- disk_name = os.path.basename(path_to_disk).split('.')[-3]
111
- for pattern in self.exclude_disks:
112
- if pattern in disk_name:
113
- break
114
- else:
115
- self.disks.append(Disk(name=disk_name, path=path_to_disk))
221
222
def check(self):
223
+ self.disks = self.scan()
224
+
225
if not self.disks:
119
- self.error('Can\'t locate any smartd log files in {0}'.format(self.log_path))
120
- return False
226
+ return None
227
+
228
+ user_defined_sa = self.configuration.get('smart_attributes')
229
+
230
+ if user_defined_sa:
231
+ order = user_defined_sa.split() or ORDER
232
+ else:
233
+ order = ORDER
234
+
235
+ self.create_charts(order)
236
122
- self.create_charts()
237
return True
238
239
def get_data(self):
240
+ self.runs += 1
241
+
242
+ if self.runs % RESCAN_INTERVAL == 0:
243
+ self.cleanup_and_rescan()
244
+
245
data = dict()
246
+
247
for disk in self.disks:
248
+
249
if not disk.status:
250
continue
251
131
- try:
132
- last_line = read_last_line(disk.path)
133
- except OSError:
252
+ changed = disk.log_file.is_changed()
253
+
254
+ # True = changed, False = unchanged, None = Exception
255
+ if changed is None:
256
disk.status = False
257
continue
258
137
- result = self.regex.findall(last_line)
138
- if not result:
139
- continue
140
- for a, n, r in result:
141
- data.update({'_'.join([disk.name, a]): r if self.raw_values else n})
259
+ if changed:
260
+ success = disk.get_attributes()
261
+ if not success:
262
+ disk.status = False
263
+ continue
264
+
265
+ data.update(disk.data(self.raw))
266
267
return data or None
268
145
- def create_charts(self):
269
+ def create_charts(self, order):
270
+ for attr in order:
271
+ chart_id = 'attr_id' + attr
272
+ chart = chart_template(attr, self.raw)
273
147
- def create_lines(attr_id):
148
- result = list()
274
for disk in self.disks:
150
- result.append(['_'.join([disk.name, attr_id]), disk.name, 'absolute'])
151
- return result
275
+ if attr not in disk.attributes:
276
+ self.debug("'{disk}' has no attribute '{attr_id}'".format(disk=disk.name,
277
+ attr_id=attr))
278
+ continue
279
153
- try:
154
- order = [attr for attr in self.attr.split() if attr in SMART_ATTR.keys()] or ORDER
155
- except AttributeError:
156
- order = ORDER
280
+ if self.raw and disk.attributes[attr].raw is None:
281
+ self.debug("'{disk}' attribute '{attr_id}' value not in {limits}".format(disk=disk.name,
282
+ attr_id=attr,
283
+ limits=LIMITS[attr]))
284
+ continue
285
+ chart[chart_id]['lines'].append(['_'.join([disk.name, attr]), disk.name])
286
+
287
+ self.order.append(chart_id)
288
+ self.definitions.update(chart)
289
+
290
+ def scan(self, only_new=None):
291
+ new_disks = list()
292
+ for f in os.listdir(self.log_path):
293
+ full_path = os.path.join(self.log_path, f)
294
+
295
+ if DiskLogFile.is_valid(full_path, self.exclude):
296
+ disk = Disk(full_path, self.age)
297
+
298
+ active = disk.is_active()
299
+ if active is None:
300
+ continue
301
+
302
+ if active:
303
+ if not only_new:
304
+ new_disks.append(disk)
305
+ else:
306
+ if disk not in self.disks:
307
+ new_disks.append(disk)
308
+ else:
309
+ if not only_new:
310
+ self.debug("'{disk}' not updated in the last {age} minutes, "
311
+ "skipping it.".format(disk=disk.name, age=self.age))
312
+ return new_disks
313
+
314
+ def cleanup_and_rescan(self):
315
+ self.cleanup()
316
+ new_disks = self.scan(only_new=True)
317
+
318
+ for disk in new_disks:
319
+ valid = False
320
+
321
+ for chart in self.charts:
322
+ idx = chart.id[7:]
323
+
324
+ if idx in disk.attributes:
325
+ valid = True
326
+ dimension_id = '_'.join([disk.name, idx])
327
+
328
+ if dimension_id in chart:
329
+ chart.hide_dimension(dimension_id=dimension_id, reverse=True)
330
+ else:
331
+ chart.add_dimension([dimension_id, disk.name])
332
+ if valid:
333
+ self.disks.append(disk)
334
+
335
+ def cleanup(self):
336
+ for disk in self.disks:
337
+
338
+ if not disk.is_active():
339
+ disk.status = False
340
+
341
+ if not disk.status:
342
+ for chart in self.charts:
343
+ dimension_id = '_'.join([disk.name, chart.id[7:]])
344
+ chart.hide_dimension(dimension_id=dimension_id)
345
158
- self.order = [''.join(['attr_id', i]) for i in order]
159
- units = 'raw' if self.raw_values else 'normalized'
160
-
161
- for k, v in dict([(k, v) for k, v in SMART_ATTR.items() if k in order]).items():
162
- self.definitions[''.join(['attr_id', k])] =\
163
- {'options': [None, v, units, v.lower(), 'smartd.attr_id' + k, 'line'],
164
- 'lines': create_lines(k)}
165
-
166
-
167
-def find_disks_in_log_path(log_path):
168
- if not os.path.isdir(log_path):
169
- raise StopIteration
170
- for f in os.listdir(log_path):
171
- f = os.path.join(log_path, f)
172
- if all([os.path.isfile(f),
173
- os.access(f, os.R_OK),
174
- f.endswith('.csv'),
175
- os.path.getsize(f)]):
176
- yield f
346
+ self.disks = [disk for disk in self.disks if disk.status]