@cryptotaxi247 / netdata-1 / commits / ddca52216

smartd_log plugin: initial version added

Ilya committed Feb 1, 2017 at 15:19 UTC ddca522169be26780475be170564aed1ca107fea
1 file changed +220
python.d/smartd_log.chart.py new
+220
@@ -0,0 +1,220 @@
1 +# -*- coding: utf-8 -*-
2 +# Description: smart netdata python.d module
3 +# Author: l2isbad, vorph1
4 +
5 +from re import compile
6 +from os import listdir, access, R_OK
7 +from os.path import isfile, join, getsize, basename
8 +try:
9 + from queue import Queue
10 +except ImportError:
11 + from Queue import Queue
12 +from threading import Thread
13 +from base import SimpleService
14 +from collections import namedtuple
15 +
16 +# default module values (can be overridden per job in `config`)
17 +update_every = 5
18 +priority = 60000
19 +
20 +# charts order (can be overridden if you want less charts, or different order)
21 +ORDER = ['1', '4', '5', '7', '9', '12', '193', '194', '197', '198', '200']
22 +
23 +SMART_ATTR = {
24 + '1': 'Read Error Rate',
25 + '2': 'Throughput Performance',
26 + '3': 'Spin-Up Time',
27 + '4': 'Start/Stop Count',
28 + '5': 'Reallocated Sectors Count',
29 + '6': 'Read Channel Margin',
30 + '7': 'Seek Error Rate',
31 + '8': 'Seek Time Performance',
32 + '9': 'Power-On Hours Count',
33 + '10': 'Spin-up Retries',
34 + '11': 'Calibration Retries',
35 + '12': 'Power Cycle Count',
36 + '13': 'Soft Read Error Rate',
37 + '100': 'Erase/Program Cycles',
38 + '103': 'Translation Table Rebuild',
39 + '108': 'Unknown (108)',
40 + '170': 'Reserved Block Count',
41 + '171': 'Program Fail Count',
42 + '172': 'Erase Fail Count',
43 + '173': 'Wear Leveller Worst Case Erase Count',
44 + '174': 'Unexpected Power Loss',
45 + '175': 'Program Fail Count',
46 + '176': 'Erase Fail Count',
47 + '177': 'Wear Leveling Count',
48 + '178': 'Used Reserved Block Count',
49 + '179': 'Used Reserved Block Count',
50 + '180': 'Unused Reserved Block Count',
51 + '181': 'Program Fail Count',
52 + '182': 'Erase Fail Count',
53 + '183': 'SATA Downshifts',
54 + '184': 'End-to-End error',
55 + '185': 'Head Stability',
56 + '186': 'Induced Op-Vibration Detection',
57 + '187': 'Reported Uncorrectable Errors',
58 + '188': 'Command Timeout',
59 + '189': 'High Fly Writes',
60 + '190': 'Temperature',
61 + '191': 'G-Sense Errors',
62 + '192': 'Power-Off Retract Cycles',
63 + '193': 'Load/Unload Cycles',
64 + '194': 'Temperature',
65 + '195': 'Hardware ECC Recovered',
66 + '196': 'Reallocation Events',
67 + '197': 'Current Pending Sectors',
68 + '198': 'Off-line Uncorrectable',
69 + '199': 'UDMA CRC Error Rate',
70 + '200': 'Write Error Rate',
71 + '201': 'Soft Read Errors',
72 + '202': 'Data Address Mark Errors',
73 + '203': 'Run Out Cancel',
74 + '204': 'Soft ECC Corrections',
75 + '205': 'Thermal Asperity Rate',
76 + '206': 'Flying Height',
77 + '207': 'Spin High Current',
78 + '209': 'Offline Seek Performance',
79 + '220': 'Disk Shift',
80 + '221': 'G-Sense Error Rate',
81 + '222': 'Loaded Hours',
82 + '223': 'Load/Unload Retries',
83 + '224': 'Load Friction',
84 + '225': 'Load/Unload Cycles',
85 + '226': 'Load-in Time',
86 + '227': 'Torque Amplification Count',
87 + '228': 'Power-Off Retracts',
88 + '230': 'GMR Head Amplitude',
89 + '231': 'Temperature',
90 + '232': 'Available Reserved Space',
91 + '233': 'Media Wearout Indicator',
92 + '240': 'Head Flying Hours',
93 + '241': 'Total LBAs Written',
94 + '242': 'Total LBAs Read',
95 + '250': 'Read Error Retry Rate'
96 +}
97 +
98 +NAMED_DISKS = namedtuple('disks', ['name', 'size', 'number'])
99 +
100 +
101 +class Service(SimpleService):
102 + def __init__(self, configuration=None, name=None):
103 + SimpleService.__init__(self, configuration=configuration, name=name)
104 + self.regex = compile(r'(\d+);(\d+);(\d+)')
105 + self.log_path = self.configuration.get('log_path', '/var/log/smartd')
106 + self.raw_values = self.configuration.get('raw_values')
107 + self.attr = self.configuration.get('smart_attributes', [])
108 + self.previous_data = dict()
109 +
110 + def check(self):
111 + # Can\'t start without smartd readable diks log files
112 + disks = find_disks_in_log_path(self.log_path)
113 + if not disks:
114 + self.error('Can\'t locate any smartd log files in %s' % self.log_path)
115 + return False
116 +
117 + # List of namedtuples to track smartd log file size
118 + self.disks = [NAMED_DISKS(name=disks[i], size=0, number=i) for i in range(len(disks))]
119 +
120 + if self._get_data():
121 + self.create_charts()
122 + return True
123 + else:
124 + self.error('Can\'t collect any data. Sorry.')
125 + return False
126 +
127 + def _get_raw_data(self, queue, disk):
128 + # The idea is to open a file.
129 + # Jump to the end.
130 + # Seek backward until '\n' symbol appears
131 + # If '\n' is found or it's the beginning of the file
132 + # readline()! (last or first line)
133 + with open(disk, 'rb') as f:
134 + f.seek(-2, 2)
135 + while f.read(1) != b'\n':
136 + f.seek(-2, 1)
137 + if f.tell() == 0:
138 + break
139 + result = f.readline()
140 +
141 + result = result.decode(encoding='utf-8')
142 + result = self.regex.findall(result)
143 +
144 + queue.put([basename(disk), result])
145 +
146 + def _get_data(self):
147 + threads, result = list(), list()
148 + queue = Queue()
149 + to_netdata = dict()
150 +
151 + # If the size has not changed there is no reason to poll log files.
152 + disks = [disk for disk in self.disks if self.size_changed(disk)]
153 + if disks:
154 + for disk in disks:
155 + th = Thread(target=self._get_raw_data, args=(queue, disk.name))
156 + th.start()
157 + threads.append(th)
158 +
159 + for thread in threads:
160 + thread.join()
161 + result.append(queue.get())
162 + else:
163 + # Data from last real poll
164 + return self.previous_data or None
165 +
166 + for elem in result:
167 + for a, n, r in elem[1]:
168 + to_netdata.update({'_'.join([elem[0], a]): r if self.raw_values else n})
169 +
170 + self.previous_data.update(to_netdata)
171 +
172 + return to_netdata or None
173 +
174 + def size_changed(self, disk):
175 + # We are not interested in log files:
176 + # 1. zero size
177 + # 2. size is not changed since last poll
178 + try:
179 + size = getsize(disk.name)
180 + if size != disk.size and size:
181 + self.disks[disk.number] = disk._replace(size=size)
182 + return True
183 + else:
184 + return False
185 + except OSError:
186 + # Remove unreadable/nonexisting log files from list of disks and previous_data
187 + self.disks.remove(disk)
188 + self.previous_data = {k: v for k, v in self.previous_data.items() if basename(disk.name) not in k}
189 + return False
190 +
191 + def create_charts(self):
192 +
193 + def create_lines(attrid):
194 + result = list()
195 + for disk in self.disks:
196 + name = basename(disk.name)
197 + result.append(['_'.join([name, attrid]), name[:name.index('.')], 'absolute'])
198 + return result
199 +
200 + # Add additional smart attributes to the ORDER. If something goes wrong we don't care.
201 + try:
202 + ORDER.extend(list(set(self.attr.split()) & SMART_ATTR.keys() - set(ORDER)))
203 + except Exception:
204 + pass
205 + self.order = [''.join(['attrid', i]) for i in ORDER]
206 + self.definitions = dict()
207 + units = 'raw' if self.raw_values else 'normalized'
208 +
209 + for k, v in {k: v for k, v in SMART_ATTR.items() if k in ORDER}.items():
210 + self.definitions.update({''.join(['attrid', k]): {
211 + 'options': [None, v, units, v, 'smartd.attrid' + k, 'line'],
212 + 'lines': create_lines(k)}})
213 +
214 +def find_disks_in_log_path(log_path):
215 + # smartd log file is OK if:
216 + # 1. it is a file
217 + # 2. file name endswith with 'csv'
218 + # 3. file is readable
219 + return [join(log_path, f) for f in listdir(log_path)
220 + if all([isfile(join(log_path, f)), f.endswith('.csv'), access(join(log_path, f), R_OK)])]