charts classes added
lgz committed
Oct 13, 2017 at 00:06 UTC
53d42b709d19a7642551a4df93e9ed090462d1a0
1 file changed
+234
python.d/python_modules/bases/charts.py
new
+234
@@ -0,0 +1,234 @@
1
+# -*- coding: utf-8 -*-
2
+# Description:
3
+# Author: Ilya Mashchenko (l2isbad)
4
+
5
+from bases.collection import safe_print
6
+
7
+CHART_PARAMS = ['type', 'id', 'name', 'title', 'units', 'family', 'context', 'chart_type']
8
+DIMENSION_PARAMS = ['id', 'name', 'algorithm', 'multiplier', 'divisor']
9
+CHART_TYPES = ['line', 'area', 'stacked']
10
+DIMENSION_ALGORITHMS = ['absolute', 'incremental', 'percentage-of-absolute-row', 'percentage-of-incremental-row']
11
+
12
+CHART_BEGIN = 'BEGIN {type}.{id} {since_last}\n'
13
+CHART_CREATE = "CHART {type}.{id} '{name}' '{title}' '{units}' '{family}' '{context}' " \
14
+ "{chart_type} {priority} {update_every}\n"
15
+CHART_OBSOLETE = "CHART {type}.{id} '{name}' '{title}' '{units}' '{family}' '{context}' " \
16
+ "{chart_type} {priority} {update_every} 'obsolete'\n"
17
+
18
+
19
+DIMENSION_CREATE = "DIMENSION '{id}' '{name}' {algorithm} {multiplier} {divisor} '{hidden}'\n"
20
+DIMENSION_SET = "SET '{id}' = {value}\n"
21
+
22
+RUNTIME_CHART_CREATE = "CHART netdata.runtime_{job_name} '' 'Execution time for {job_name}' 'ms' 'python.d' " \
23
+ "netdata.pythond_runtime line 145000 {update_every}\n" \
24
+ "DIMENSION run_time 'run time' absolute 1 1\n"
25
+
26
+
27
+def create_runtime_chart(func):
28
+ """
29
+ :param func: class method
30
+ :return:
31
+ """
32
+ def wrapper(*args, **kwargs):
33
+ self = args[0]
34
+ ok = func(*args, **kwargs)
35
+ if ok:
36
+ self.safe_print(RUNTIME_CHART_CREATE.format(job_name=self.name,
37
+ update_every=self._runtime_counters.FREQ))
38
+ return ok
39
+ return wrapper
40
+
41
+
42
+class ReAddingError(Exception):
43
+ pass
44
+
45
+
46
+class Checks:
47
+ @staticmethod
48
+ def chart_params(params, chart_name):
49
+ """
50
+ :param params: <list>
51
+ :param chart_name: <str>
52
+ :return:
53
+ """
54
+ for param in CHART_PARAMS:
55
+ try:
56
+ params[param]
57
+ except KeyError:
58
+ raise KeyError('{param} is missing ({chart})'.format(param=param, chart=chart_name))
59
+ value = params[param]
60
+ if any([param == 'id', param == 'units', param == 'context', param == 'chart_type']):
61
+ if not value:
62
+ raise ValueError('Wrong chart value ({chart}). {param} must not be empty'.format(chart=chart_name,
63
+ param=param))
64
+ if param == 'chart_type':
65
+ if value not in CHART_TYPES:
66
+ raise ValueError('Wrong chart type ({chart}). Must be on of {types}'.format(chart=chart_name,
67
+ types=CHART_TYPES))
68
+
69
+ @staticmethod
70
+ def dimension_params(params, chart_name):
71
+ """
72
+ :param params: <list>
73
+ :param chart_name: <str>
74
+ :return:
75
+ """
76
+ if not params.get('id'):
77
+ raise ValueError('Wrong dimension id ({chart}). Must not be empty'.format(chart=chart_name))
78
+ if params['algorithm'] not in DIMENSION_ALGORITHMS:
79
+ raise ValueError('Wrong dimension algorithm ({chart}). '
80
+ 'Must be one of {algorithms}'.format(chart=chart_name,
81
+ algorithms=DIMENSION_ALGORITHMS))
82
+ if not (str(params['multiplier']).isdigit() and str(params['divisor']).isdigit()):
83
+ raise ValueError('Wrong multiplier or divisor ({chart}). Must be a digit'.format(chart=chart_name))
84
+
85
+
86
+class Charts:
87
+ def __init__(self, job_name, priority, update_every):
88
+ """
89
+ :param job_name: <str>
90
+ :param priority: <int>
91
+ :param update_every: <int>
92
+ """
93
+ self.job_name = job_name
94
+ self.priority = priority
95
+ self.update_every = update_every
96
+ self.charts = dict()
97
+
98
+ def __len__(self):
99
+ return len(self.charts)
100
+
101
+ def __iter__(self):
102
+ return iter(self.charts.values())
103
+
104
+ def __repr__(self):
105
+ return str([chart for chart in self.charts])
106
+
107
+ def __contains__(self, item):
108
+ return item in self.charts
109
+
110
+ def __getitem__(self, item):
111
+ return self.charts[item]
112
+
113
+ def __delitem__(self, key):
114
+ del self.charts[key]
115
+
116
+ def empty(self):
117
+ return not bool(self.charts)
118
+
119
+ def penalty_exceeded(self, penalty_max):
120
+ """
121
+ :param penalty_max: <int>
122
+ :return:
123
+ """
124
+ return (chart for chart in self if chart.penalty > penalty_max)
125
+
126
+ def add_chart(self, params):
127
+ """
128
+ :param params: <list>
129
+ :return:
130
+ """
131
+ params = [self.job_name] + params
132
+ chart_id = params[1]
133
+ if chart_id in self.charts:
134
+ raise ReAddingError('{chart} already in charts'.format(chart=chart_id))
135
+ else:
136
+ new_chart = Chart(params)
137
+ new_chart.params['update_every'] = self.update_every
138
+ new_chart.params['priority'] = self.priority
139
+ self.priority += 1
140
+ self.charts[new_chart.params['id']] = new_chart
141
+ return new_chart
142
+
143
+
144
+class Chart:
145
+ def __init__(self, params):
146
+ """
147
+ :param params: <list>
148
+ """
149
+ self.params = dict(zip(CHART_PARAMS, (p or str() for p in params)))
150
+ self.name = '.'.join([self.params['type'], self.params['id']])
151
+ Checks.chart_params(self.params, self.name)
152
+ self.dimensions = list()
153
+ self.penalty = 0
154
+
155
+ def __repr__(self):
156
+ return str(self.params)
157
+
158
+ def __str__(self):
159
+ return self.params['id']
160
+
161
+ def add_dimension(self, dimension):
162
+ """
163
+ :param dimension: <list>
164
+ :return:
165
+ """
166
+ if dimension[0] in self.dimensions:
167
+ raise ReAddingError('{dimension} already in {chart} dimensions'.format(dimension=dimension[0],
168
+ chart=self.name))
169
+ self.dimensions.append(Dimension(dimension, self.name))
170
+
171
+ def add_dimension_and_push_chart(self, dimension):
172
+ """
173
+ :param dimension: <list>
174
+ :return:
175
+ """
176
+ if dimension[0] in self.dimensions:
177
+ raise ReAddingError('{dimension} already in {chart} dimensions'.format(dimension=dimension[0],
178
+ chart=self.name))
179
+ dimension = Dimension(dimension, self.name)
180
+ self.dimensions.append(dimension)
181
+ safe_print(self.create(dimension))
182
+
183
+ def create(self, dimension=None):
184
+ """
185
+ :param dimension: <list>
186
+ :return:
187
+ """
188
+ chart = CHART_CREATE.format(**self.params)
189
+ if not dimension:
190
+ dimensions = ''.join([dimension.create() for dimension in self.dimensions])
191
+ else:
192
+ dimensions = dimension.create()
193
+ return chart + dimensions
194
+
195
+ def begin(self, since_last):
196
+ """
197
+ :param since_last: <int>: microseconds
198
+ :return:
199
+ """
200
+ return CHART_BEGIN.format(type=self.params['type'],
201
+ id=self.params['id'],
202
+ since_last=since_last)
203
+
204
+ def obsolete(self):
205
+ return CHART_OBSOLETE.format(**self.params)
206
+
207
+
208
+class Dimension:
209
+ def __init__(self, params, chart_name):
210
+ """
211
+ :param params: <list>
212
+ :param chart_name: <str>
213
+ """
214
+ self.params = dict(zip(DIMENSION_PARAMS, (p or str() for p in params)))
215
+ self.params['name'] = self.params.get('name') or self.params.get('id')
216
+ self.params.setdefault('algorithm', 'incremental')
217
+ self.params.setdefault('multiplier', 1)
218
+ self.params.setdefault('divisor', 1)
219
+ self.params.setdefault('hidden', '')
220
+ Checks.dimension_params(self.params, chart_name)
221
+
222
+ def __repr__(self):
223
+ return self.params['id']
224
+
225
+ def create(self):
226
+ return DIMENSION_CREATE.format(**self.params)
227
+
228
+ def set(self, value):
229
+ """
230
+ :param value: <str>: must be a digit
231
+ :return:
232
+ """
233
+ return DIMENSION_SET.format(id=self.params['id'],
234
+ value=value)