1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
|
# -*- coding: utf-8 -*-
# Description: changefinder netdata python.d module
# Author: andrewm4894
# SPDX-License-Identifier: GPL-3.0-or-later
from json import loads
import re
from bases.FrameworkServices.UrlService import UrlService
import numpy as np
import changefinder
from scipy.stats import percentileofscore
update_every = 5
disabled_by_default = True
ORDER = [
'scores',
'flags'
]
CHARTS = {
'scores': {
'options': [None, 'ChangeFinder', 'score', 'Scores', 'scores', 'line'],
'lines': []
},
'flags': {
'options': [None, 'ChangeFinder', 'flag', 'Flags', 'flags', 'stacked'],
'lines': []
}
}
DEFAULT_PROTOCOL = 'http'
DEFAULT_HOST = '127.0.0.1:19999'
DEFAULT_CHARTS_REGEX = 'system.*'
DEFAULT_MODE = 'per_chart'
DEFAULT_CF_R = 0.5
DEFAULT_CF_ORDER = 1
DEFAULT_CF_SMOOTH = 15
DEFAULT_CF_DIFF = False
DEFAULT_CF_THRESHOLD = 99
DEFAULT_N_SCORE_SAMPLES = 14400
DEFAULT_SHOW_SCORES = False
class Service(UrlService):
def __init__(self, configuration=None, name=None):
UrlService.__init__(self, configuration=configuration, name=name)
self.order = ORDER
self.definitions = CHARTS
self.protocol = self.configuration.get('protocol', DEFAULT_PROTOCOL)
self.host = self.configuration.get('host', DEFAULT_HOST)
self.url = '{}://{}/api/v1/allmetrics?format=json'.format(self.protocol, self.host)
self.charts_regex = re.compile(self.configuration.get('charts_regex', DEFAULT_CHARTS_REGEX))
self.charts_to_exclude = self.configuration.get('charts_to_exclude', '').split(',')
self.mode = self.configuration.get('mode', DEFAULT_MODE)
self.n_score_samples = int(self.configuration.get('n_score_samples', DEFAULT_N_SCORE_SAMPLES))
self.show_scores = int(self.configuration.get('show_scores', DEFAULT_SHOW_SCORES))
self.cf_r = float(self.configuration.get('cf_r', DEFAULT_CF_R))
self.cf_order = int(self.configuration.get('cf_order', DEFAULT_CF_ORDER))
self.cf_smooth = int(self.configuration.get('cf_smooth', DEFAULT_CF_SMOOTH))
self.cf_diff = bool(self.configuration.get('cf_diff', DEFAULT_CF_DIFF))
self.cf_threshold = float(self.configuration.get('cf_threshold', DEFAULT_CF_THRESHOLD))
self.collected_dims = {'scores': set(), 'flags': set()}
self.models = {}
self.x_latest = {}
self.scores_latest = {}
self.scores_samples = {}
def get_score(self, x, model):
"""Update the score for the model based on most recent data, flag if it's percentile passes self.cf_threshold.
"""
# get score
if model not in self.models:
# initialise empty model if needed
self.models[model] = changefinder.ChangeFinder(r=self.cf_r, order=self.cf_order, smooth=self.cf_smooth)
# if the update for this step fails then just fallback to last known score
try:
score = self.models[model].update(x)
self.scores_latest[model] = score
except Exception as _:
score = self.scores_latest.get(model, 0)
score = 0 if np.isnan(score) else score
# update sample scores used to calculate percentiles
if model in self.scores_samples:
self.scores_samples[model].append(score)
else:
self.scores_samples[model] = [score]
self.scores_samples[model] = self.scores_samples[model][-self.n_score_samples:]
# convert score to percentile
score = percentileofscore(self.scores_samples[model], score)
# flag based on score percentile
flag = 1 if score >= self.cf_threshold else 0
return score, flag
def validate_charts(self, chart, data, algorithm='absolute', multiplier=1, divisor=1):
"""If dimension not in chart then add it.
"""
if not self.charts:
return
for dim in data:
if dim not in self.collected_dims[chart]:
self.collected_dims[chart].add(dim)
self.charts[chart].add_dimension([dim, dim, algorithm, multiplier, divisor])
for dim in list(self.collected_dims[chart]):
if dim not in data:
self.collected_dims[chart].remove(dim)
self.charts[chart].del_dimension(dim, hide=False)
def diff(self, x, model):
"""Take difference of data.
"""
x_diff = x - self.x_latest.get(model, 0)
self.x_latest[model] = x
x = x_diff
return x
def _get_data(self):
# pull data from self.url
raw_data = self._get_raw_data()
if raw_data is None:
return None
raw_data = loads(raw_data)
# filter to just the data for the charts specified
charts_in_scope = list(filter(self.charts_regex.match, raw_data.keys()))
charts_in_scope = [c for c in charts_in_scope if c not in self.charts_to_exclude]
data_score = {}
data_flag = {}
# process each chart
for chart in charts_in_scope:
if self.mode == 'per_chart':
# average dims on chart and run changefinder on that average
x = [raw_data[chart]['dimensions'][dim]['value'] for dim in raw_data[chart]['dimensions']]
x = [x for x in x if x is not None]
if len(x) > 0:
x = sum(x) / len(x)
x = self.diff(x, chart) if self.cf_diff else x
score, flag = self.get_score(x, chart)
if self.show_scores:
data_score['{}_score'.format(chart)] = score * 100
data_flag[chart] = flag
else:
# run changefinder on each individual dim
for dim in raw_data[chart]['dimensions']:
chart_dim = '{}|{}'.format(chart, dim)
x = raw_data[chart]['dimensions'][dim]['value']
x = x if x else 0
x = self.diff(x, chart_dim) if self.cf_diff else x
score, flag = self.get_score(x, chart_dim)
if self.show_scores:
data_score['{}_score'.format(chart_dim)] = score * 100
data_flag[chart_dim] = flag
self.validate_charts('flags', data_flag)
if self.show_scores & len(data_score) > 0:
data_score['average_score'] = sum(data_score.values()) / len(data_score)
self.validate_charts('scores', data_score, divisor=100)
data = {**data_score, **data_flag}
return data
|