"web_log" fixes
lgz committed
Oct 22, 2017 at 01:52 UTC
d234e418e20ad784822ef2ff405fdb268eb0be7d
1 file changed
+100
-166
python.d/web_log.chart.py
+100
-166
@@ -4,22 +4,20 @@
4
5
import bisect
6
import re
7
+import os
8
9
from collections import namedtuple, defaultdict
10
from copy import deepcopy
10
-from os import access, R_OK
11
-from os.path import getsize
11
12
try:
13
from itertools import filterfalse
14
except ImportError:
15
+ from itertools import ifilter as filter
16
from itertools import ifilterfalse as filterfalse
17
18
-from base import LogService
19
-import msg
18
+from bases.collection import read_last_line
19
+from bases.FrameworkServices.LogService import LogService
20
21
-priority = 60000
22
-retries = 60
21
22
ORDER_APACHE_CACHE = ['apache_cache']
23
@@ -256,8 +254,9 @@ class Service(LogService):
254
:param name:
255
"""
256
LogService.__init__(self, configuration=configuration, name=name)
259
- self.log_type = self.configuration.get('type', 'web')
257
+ self.configuration = configuration
258
self.log_path = self.configuration.get('path')
259
+ self.job = None
260
261
def check(self):
262
"""
@@ -269,123 +268,43 @@ class Service(LogService):
268
4. other checks depends on log "type"
269
"""
270
271
+ log_type = self.configuration.get('type', 'web')
272
log_types = dict(web=Web, apache_cache=ApacheCache, squid=Squid)
273
274
- if self.log_type not in log_types:
275
- self.error('bad log type (%s). Supported types: %s' % (self.log_type, log_types.keys()))
274
+ if log_type not in log_types:
275
+ self.error("bad log type {log_type}. Supported types: {types}".format(log_type=log_type,
276
+ types=log_types.keys()))
277
return False
278
279
if not self.log_path:
280
self.error('log path is not specified')
281
return False
282
282
- if not (self._find_recent_log_file() and access(self.log_path, R_OK)):
283
- self.error('%s not readable or not exist' % self.log_path)
283
+ if not (self._find_recent_log_file() and os.access(self.log_path, os.R_OK)):
284
+ self.error('{log_file} not readable or not exist'.format(log_file=self.log_path))
285
return False
286
286
- if not getsize(self.log_path):
287
- self.error('%s is empty' % self.log_path)
287
+ if not os.path.getsize(self.log_path):
288
+ self.error('{log_file} is empty'.format(log_file=self.log_path))
289
return False
290
290
- self.configuration['update_every'] = self.update_every
291
- self.configuration['name'] = self.name
292
- self.configuration['override_name'] = self.override_name
293
- self.configuration['_dimensions'] = self._dimensions
294
- self.configuration['path'] = self.log_path
295
-
296
- cls = log_types[self.log_type]
297
- self.Job = cls(configuration=self.configuration)
298
- if self.Job.check():
299
- self.order = self.Job.order
300
- self.definitions = self.Job.definitions
301
- self.info('Current log file: %s' % self.log_path)
291
+ self.job = log_types[log_type](self)
292
+ if self.job.check():
293
+ self.order = self.job.order
294
+ self.definitions = self.job.definitions
295
return True
296
return False
297
298
def _get_data(self):
306
- return self.Job.get_data(self._get_raw_data())
299
+ return self.job.get_data(self._get_raw_data())
300
301
309
-class Mixin:
310
- def filter_data(self, raw_data):
311
- """
312
- :param raw_data: list
313
- :return:
314
- """
315
- if not self.pre_filter:
316
- return raw_data
317
- filtered = raw_data
318
- for elem in self.pre_filter:
319
- if elem.description == 'filter_include':
320
- filtered = filter(elem.func, filtered)
321
- elif elem.description == 'filter_exclude':
322
- filtered = filterfalse(elem.func, filtered)
323
- return filtered
324
-
325
- def add_new_dimension(self, dimension_id, chart_key, dimension=None,
326
- algorithm='incremental', multiplier=1, divisor=1):
327
- """
328
- :param dimension:
329
- :param chart_key:
330
- :param dimension_id:
331
- :param algorithm:
332
- :param multiplier:
333
- :param divisor:
334
- :return:
335
- """
336
-
337
- self.data[dimension_id] = 0
338
- # SET method check if dim in _dimensions
339
- self.conf['_dimensions'].append(dimension_id)
340
- # UPDATE method do SET only if dim in definitions
341
- dimension_list = list(map(str, [dimension_id,
342
- dimension if dimension else dimension_id,
343
- algorithm,
344
- multiplier,
345
- divisor]))
346
- self.definitions[chart_key]['lines'].append(dimension_list)
347
- job_name = find_job_name(self.conf['override_name'], self.conf['name'])
348
- opts = self.definitions[chart_key]['options']
349
- chart = 'CHART %s.%s "" "%s" %s "%s" %s %s 60000 %s\n' % (job_name, chart_key,
350
- opts[1], opts[2], opts[3],
351
- opts[4], opts[5], self.conf['update_every'])
352
- print(chart + "DIMENSION %s\n" % ' '.join(dimension_list))
353
-
354
- def get_last_line(self):
355
- """
356
- Reads last line from the log file
357
- :return: str:
358
- """
359
- # Read last line (or first if there is only one line)
360
- with open(self.conf['path'], 'rb') as logs:
361
- logs.seek(-2, 2)
362
- while logs.read(1) != b'\n':
363
- logs.seek(-2, 1)
364
- if logs.tell() == 0:
365
- break
366
- last_line = logs.readline()
367
- try:
368
- return last_line.decode()
369
- except UnicodeDecodeError:
370
- try:
371
- return last_line.decode(encoding='utf-8')
372
- except (TypeError, UnicodeDecodeError) as error:
373
- msg.error('web_log', str(error))
374
- return False
375
-
376
- @staticmethod
377
- def error(*params):
378
- msg.error('web_log', ' '.join(map(str, params)))
379
-
380
- @staticmethod
381
- def info(*params):
382
- msg.info('web_log', ' '.join(map(str, params)))
383
-
384
-
385
-class Web(Mixin):
386
- def __init__(self, configuration):
387
- self.conf = configuration
388
- self.pre_filter = check_patterns('filter', self.conf.get('filter'))
302
+class Web:
303
+ def __init__(self, service):
304
+ self.service = service
305
+ self.order = ORDER_WEB[:]
306
+ self.definitions = deepcopy(CHARTS_WEB)
307
+ self.pre_filter = check_patterns('filter', self.configuration.get('filter'))
308
self.storage = dict()
309
self.data = {'bytes_sent': 0, 'resp_length': 0, 'resp_time_min': 0, 'resp_time_max': 0,
310
'resp_time_avg': 0, 'resp_time_upstream_min': 0, 'resp_time_upstream_max': 0,
@@ -394,23 +313,27 @@ class Web(Mixin):
313
'req_ipv6': 0, 'unique_tot_ipv4': 0, 'unique_tot_ipv6': 0, 'successful_requests': 0,
314
'redirects': 0, 'bad_requests': 0, 'server_errors': 0, 'other_requests': 0, 'GET': 0}
315
316
+ def __getattr__(self, item):
317
+ return getattr(self.service, item)
318
+
319
def check(self):
398
- last_line = self.get_last_line()
320
+ last_line = read_last_line(self.log_path)
321
if not last_line:
322
return False
323
# Custom_log_format or predefined log format.
402
- if self.conf.get('custom_log_format'):
324
+ if self.configuration.get('custom_log_format'):
325
match_dict, error = self.find_regex_custom(last_line)
326
else:
327
match_dict, error = self.find_regex(last_line)
328
329
# "match_dict" is None if there are any problems
330
if match_dict is None:
409
- self.error(str(error))
331
+ self.error(error)
332
return False
333
+
334
self.storage['unique_all_time'] = list()
412
- self.storage['url_pattern'] = check_patterns('url_pattern', self.conf.get('categories'))
413
- self.storage['user_pattern'] = check_patterns('user_pattern', self.conf.get('user_defined'))
335
+ self.storage['url_pattern'] = check_patterns('url_pattern', self.configuration.get('categories'))
336
+ self.storage['user_pattern'] = check_patterns('user_pattern', self.configuration.get('user_defined'))
337
338
self.create_web_charts(match_dict) # Create charts
339
self.info('Collected data: %s' % list(match_dict.keys()))
@@ -422,20 +345,21 @@ class Web(Mixin):
345
:return:
346
Create/remove additional charts depending on the 'match_dict' keys and configuration file options
347
"""
425
- self.order = ORDER_WEB[:]
426
- self.definitions = deepcopy(CHARTS_WEB)
427
-
348
if 'resp_time' not in match_dict:
349
self.order.remove('response_time')
350
if 'resp_time_upstream' not in match_dict:
351
self.order.remove('response_time_upstream')
352
433
- if not self.conf.get('all_time', True):
353
+ if not self.configuration.get('all_time', True):
354
self.order.remove('clients_all')
355
356
# Add 'detailed_response_codes' chart if specified in the configuration
437
- if self.conf.get('detailed_response_codes', True):
438
- codes = DET_RESP_AGGR[:1] if self.conf.get('detailed_response_aggregate', True) else DET_RESP_AGGR[1:]
357
+ if self.configuration.get('detailed_response_codes', True):
358
+ if self.configuration.get('detailed_response_aggregate', True):
359
+ codes = DET_RESP_AGGR[:1]
360
+ else:
361
+ codes = DET_RESP_AGGR[1:]
362
+
363
for code in codes:
364
self.order.append('detailed_response_codes%s' % code)
365
self.definitions['detailed_response_codes%s' % code] \
@@ -446,9 +370,8 @@ class Web(Mixin):
370
# Add 'requests_per_url' chart if specified in the configuration
371
if self.storage['url_pattern']:
372
for elem in self.storage['url_pattern']:
449
- self.definitions['requests_per_url']['lines'].append([elem.description,
450
- elem.description[12:],
451
- 'incremental'])
373
+ dim = [elem.description, elem.description[12:], 'incremental']
374
+ self.definitions['requests_per_url']['lines'].append(dim)
375
self.data[elem.description] = 0
376
self.data['url_pattern_other'] = 0
377
else:
@@ -457,9 +380,8 @@ class Web(Mixin):
380
# Add 'requests_per_user_defined' chart if specified in the configuration
381
if self.storage['user_pattern'] and 'user_defined' in match_dict:
382
for elem in self.storage['user_pattern']:
460
- self.definitions['requests_per_user_defined']['lines'].append([elem.description,
461
- elem.description[13:],
462
- 'incremental'])
383
+ dim = [elem.description, elem.description[13:], 'incremental']
384
+ self.definitions['requests_per_user_defined']['lines'].append(dim)
385
self.data[elem.description] = 0
386
self.data['user_pattern_other'] = 0
387
else:
@@ -475,7 +397,7 @@ class Web(Mixin):
397
if not raw_data:
398
return None if raw_data is None else self.data
399
478
- filtered_data = self.filter_data(raw_data=raw_data)
400
+ filtered_data = filter_data(raw_data=raw_data, pre_filter=self.pre_filter)
401
402
unique_current = set()
403
timings = defaultdict(lambda: dict(minimum=None, maximum=0, summary=0, count=0))
@@ -490,7 +412,7 @@ class Web(Mixin):
412
except KeyError:
413
self.data['0xx'] += 1
414
# detailed response code
493
- if self.conf.get('detailed_response_codes', True):
415
+ if self.configuration.get('detailed_response_codes', True):
416
self.get_data_per_response_codes_detailed(code=match_dict['code'])
417
# response statuses
418
self.get_data_per_statuses(code=match_dict['code'])
@@ -518,7 +440,7 @@ class Web(Mixin):
440
proto = 'ipv6' if ':' in match_dict['address'] else 'ipv4'
441
self.data['req_' + proto] += 1
442
# unique clients ips
521
- if self.conf.get('all_time', True):
443
+ if self.configuration.get('all_time', True):
444
if address_not_in_pool(pool=self.storage['unique_all_time'],
445
address=match_dict['address'],
446
pool_size=self.data['unique_tot_ipv4'] + self.data['unique_tot_ipv6']):
@@ -644,14 +566,14 @@ class Web(Mixin):
566
("resp_length" is integer or "-", "resp_time" is integer or float)
567
568
"""
647
- if not hasattr(self.conf.get('custom_log_format'), 'keys'):
569
+ if not hasattr(self.configuration.get('custom_log_format'), 'keys'):
570
return find_regex_return(msg='Custom log: "custom_log_format" is not a <dict>')
571
650
- pattern = self.conf.get('custom_log_format', dict()).get('pattern')
572
+ pattern = self.configuration.get('custom_log_format', dict()).get('pattern')
573
if not (pattern and isinstance(pattern, str)):
574
return find_regex_return(msg='Custom log: "pattern" option is not specified or type is not <str>')
575
654
- resp_time_func = self.conf.get('custom_log_format', dict()).get('time_multiplier') or 0
576
+ resp_time_func = self.configuration.get('custom_log_format', dict()).get('time_multiplier') or 0
577
578
if not isinstance(resp_time_func, int):
579
return find_regex_return(msg='Custom log: "time_multiplier" is not an integer')
@@ -660,6 +582,7 @@ class Web(Mixin):
582
regex = re.compile(pattern)
583
except re.error as error:
584
return find_regex_return(msg='Pattern compile error: %s' % str(error))
585
+
586
match = regex.search(last_line)
587
if not match:
588
return find_regex_return(msg='Custom log: pattern search FAILED')
@@ -716,16 +639,19 @@ class Web(Mixin):
639
# requests per http method
640
if match_dict.get('method'):
641
if match_dict['method'] not in self.data:
719
- self.add_new_dimension(dimension_id=match_dict['method'],
720
- chart_key='http_method')
642
+ self.charts['http_method'].add_dimension_and_push_chart([match_dict['method'],
643
+ match_dict['method'],
644
+ 'incremental'])
645
+ self.data[match_dict['method']] = 0
646
self.data[match_dict['method']] += 1
647
# requests per http version
648
if match_dict.get('http_version'):
649
dim_id = match_dict['http_version'].replace('.', '_')
650
if dim_id not in self.data:
726
- self.add_new_dimension(dimension_id=dim_id,
727
- chart_key='http_version',
728
- dimension=match_dict['http_version'])
651
+ self.charts['http_version'].add_dimension_and_push_chart([dim_id,
652
+ match_dict['http_version'],
653
+ 'incremental'])
654
+ self.data[dim_id] = 0
655
self.data[dim_id] += 1
656
657
def get_data_per_response_codes_detailed(self, code):
@@ -735,14 +661,14 @@ class Web(Mixin):
661
Calls add_new_dimension method If the value is found for the first time
662
"""
663
if code not in self.data:
738
- if self.conf.get('detailed_response_aggregate', True):
739
- self.add_new_dimension(dimension_id=code,
740
- chart_key='detailed_response_codes')
664
+ if self.configuration.get('detailed_response_aggregate', True):
665
+ self.charts['detailed_response_codes'].add_dimension_and_push_chart([code, code, 'incremental'])
666
+ self.data[code] = 0
667
else:
668
code_index = int(code[0]) if int(code[0]) < 6 else 6
669
chart_key = 'detailed_response_codes' + DET_RESP_AGGR[code_index]
744
- self.add_new_dimension(dimension_id=code,
745
- chart_key=chart_key)
670
+ self.charts[chart_key].add_dimension_and_push_chart([code, code, 'incremental'])
671
+ self.data[code] = 0
672
self.data[code] += 1
673
674
def get_data_per_pattern(self, row, other, pattern):
@@ -782,8 +708,8 @@ class Web(Mixin):
708
709
710
class ApacheCache:
785
- def __init__(self, configuration):
786
- self.conf = configuration
711
+ def __init__(self, service):
712
+ self.service = service
713
self.order = ORDER_APACHE_CACHE
714
self.definitions = CHARTS_APACHE_CACHE
715
@@ -807,12 +733,12 @@ class ApacheCache:
733
return data
734
735
810
-class Squid(Mixin):
811
- def __init__(self, configuration):
812
- self.conf = configuration
736
+class Squid:
737
+ def __init__(self, service):
738
+ self.service = service
739
self.order = ORDER_SQUID
740
self.definitions = CHARTS_SQUID
815
- self.pre_filter = check_patterns('filter', self.conf.get('filter'))
741
+ self.pre_filter = check_patterns('filter', self.configuration.get('filter'))
742
self.storage = dict()
743
self.data = {'duration_max': 0, 'duration_avg': 0, 'duration_min': 0, 'bytes': 0,
744
'0xx': 0, '1xx': 0, '2xx': 0, '3xx': 0, '4xx': 0, '5xx': 0,
@@ -821,8 +747,11 @@ class Squid(Mixin):
747
'redirects': 0, 'bad_requests': 0, 'server_errors': 0, 'other_requests': 0
748
}
749
750
+ def __getattr__(self, item):
751
+ return getattr(self.service, item)
752
+
753
def check(self):
825
- last_line = self.get_last_line()
754
+ last_line = read_last_line(self.log_path)
755
if not last_line:
756
return False
757
self.storage['unique_all_time'] = list()
@@ -858,7 +787,7 @@ class Squid(Mixin):
787
'chart': 'squid_mime_type',
788
'func_dim_id': lambda v: v.split('/')[0],
789
'func_dim': None}}
861
- if not self.conf.get('all_time', True):
790
+ if not self.configuration.get('all_time', True):
791
self.order.remove('squid_clients_all')
792
return True
793
@@ -866,7 +795,7 @@ class Squid(Mixin):
795
if not raw_data:
796
return None if raw_data is None else self.data
797
869
- filtered_data = self.filter_data(raw_data=raw_data)
798
+ filtered_data = filter_data(raw_data=raw_data, pre_filter=self.pre_filter)
799
800
unique_ip = set()
801
timings = defaultdict(lambda: dict(minimum=None, maximum=0, summary=0, count=0))
@@ -890,7 +819,7 @@ class Squid(Mixin):
819
820
proto = 'ipv4' if '.' in match['client_address'] else 'ipv6'
821
# unique clients ips
893
- if self.conf.get('all_time', True):
822
+ if self.configuration.get('all_time', True):
823
if address_not_in_pool(pool=self.storage['unique_all_time'],
824
address=match['client_address'],
825
pool_size=self.data['unique_tot_ipv4'] + self.data['unique_tot_ipv6']):
@@ -906,9 +835,10 @@ class Squid(Mixin):
835
dimension_id = values['func_dim_id'](match[key]) if values['func_dim_id'] else match[key]
836
if dimension_id not in self.data:
837
dimension = values['func_dim'](match[key]) if values['func_dim'] else dimension_id
909
- self.add_new_dimension(dimension_id=dimension_id,
910
- chart_key=values['chart'],
911
- dimension=dimension)
838
+ self.charts[values['chart']].add_dimension_and_push_chart([dimension_id,
839
+ dimension,
840
+ 'incremental'])
841
+ self.data[dimension_id] = 0
842
self.data[dimension_id] += 1
843
else:
844
self.data['unmatched'] += 1
@@ -942,8 +872,8 @@ class Squid(Mixin):
872
:return:
873
"""
874
if code not in self.data:
945
- self.add_new_dimension(dimension_id=code,
946
- chart_key='squid_code')
875
+ self.charts['squid_code'].add_dimension_and_push_chart([code, code, 'incremental'])
876
+ self.data[code] = 0
877
self.data[code] += 1
878
879
for tag in code.split('_'):
@@ -953,9 +883,8 @@ class Squid(Mixin):
883
continue
884
dimension_id = '_'.join(['code_detailed', tag])
885
if dimension_id not in self.data:
956
- self.add_new_dimension(dimension_id=dimension_id,
957
- dimension=tag,
958
- chart_key=chart_key)
886
+ self.charts[chart_key].add_dimension_and_push_chart([dimension_id, tag, 'incremental'])
887
+ self.data[dimension_id] = 0
888
self.data[dimension_id] += 1
889
890
@@ -1040,14 +969,19 @@ def check_patterns(string, dimension_regex_dict):
969
return result or None
970
971
1043
-def find_job_name(override_name, name):
972
+def filter_data(raw_data, pre_filter):
973
"""
1045
- :param override_name: str: 'name' var from configuration file
1046
- :param name: str: 'job_name' from configuration file
1047
- :return: str: new job name
1048
- We need this for dynamic charts. Actually the same logic as in python.d.plugin.
974
+ :param raw_data:
975
+ :param pre_filter:
976
+ :return:
977
"""
1050
- add_to_name = override_name or name
1051
- if add_to_name:
1052
- return '_'.join(['web_log', re.sub('\s+', '_', add_to_name)])
1053
- return 'web_log'
978
+
979
+ if not pre_filter:
980
+ return raw_data
981
+ filtered = raw_data
982
+ for elem in pre_filter:
983
+ if elem.description == 'filter_include':
984
+ filtered = filter(elem.func, filtered)
985
+ elif elem.description == 'filter_exclude':
986
+ filtered = filterfalse(elem.func, filtered)
987
+ return filtered