class DataWrapper(object): def __init__(self, token, conf): self.token = token self.conf = conf self.historic_data = None client = ApiClient(self.token, timeout=(3.05, 18)) self.device = Device(self.token) self.tag = Tag(self.token) self.metrics = Metrics(client) self._validation() def _validation(self): """Check the following - metric for every piece in infrastructure - That the configuration contains everything necessary. - check that there are no none values in any lists. """ def _get_devices(self, infra_conf): """Takes the configuration part for each infrastructure""" raw_devices = self.device.list() if infra_conf.get('tag'): tags = self.tag.list() tag_id =[tag['_id'] for tag in tags if tag['name'] == infra_conf['tag']] if not tag_id: available_tags = '\n'.join(list(set([tag['name'] for tag in tags]))) message = 'There is no tag with the name "{}". Try one of these: \n{}'.format(infra_conf['tag'], available_tags) raise Exception(message) else: tag_id = tag_id[0] devices = [device for device in raw_devices if tag_id in device.get('tags', [])] if not devices: available_tags = '\n'.join(list(set([tag['name'] for tag in tags]))) raise Exception('There is no device with this tag name. Try one of these: \n{}'.format(available_tags)) elif infra_conf.get('group'): group = infra_conf.get('group') devices = [device for device in raw_devices if group == device.get('group')] if not devices: groups = set([device['group'] for device in raw_devices if not device['group'] is None]) groups = '\n'.join(list(groups)) raise Exception('There is no device with this group name. The following groups are available:\n {}'.format(groups)) else: raise Exception('You have to provide either group or tag for each part of your infrastructure.') return devices def _merge_loadbalanced_data(self, data_points, points): max_length = len(data_points) for i, point in enumerate(points): if i < max_length: data_points[i] = data_points[i] + point return data_points def _get_metrics(self, metric, devices): """For all devices associated with the group or device""" metric = metric.split('.') metric_filter = self.metric_filter(metric) end = dt.datetime.now() start = end - timedelta(hours=self.conf['general'].get('timeframe', 24)) data_entries = [] for device in devices: data = self.metrics.get(device['_id'], start, end, metric_filter) data = self._data_node(data) time.sleep(0.3) if data['data']: data_entries.append(data) if not data_entries: metric = '.'.join(metric) # Append zero data to avoid zerodivison error data_entries.append({'data': [{'x': 0, 'y': 0}]}) logging.warning('No server in this group has any data on {}'.format(metric)) return data_entries def _data_node(self, data, names=None): """Inputs the data from the metrics endpoints and returns the node that has contains the data + names of the metrics.""" if not names: names = [] for d in data: if d.get('data') or d.get('data') == []: names.append(d.get('name')) d['full_name'] = names return d else: names.append(d.get('name')) return self._data_node(d.get('tree'), names) def _get_data_points(self, cumulative, data_entries, multiplier): """Extract the singular points into a list and return a list of those points""" data_points = [] for data in data_entries: points = [point['y'] * multiplier for point in data['data']] if cumulative and len(data_points) > 0: self._merge_loadbalanced_data(data_points, points) else: data_points.extend(points) return data_points def _round(self, number): rounding = self.conf['general'].get('round', 2) if rounding > 0: return round(number, rounding) else: return int(round(number, 0)) def calc_average(self, data_points): return self._round(sum(data_points) / len(data_points)) def calc_max(self, data_points): return self._round(max(data_points)) def calc_min(self, data_points): return self._round(min(data_points)) def calc_median(self, data_points): data_points.sort() start = len(data_points) // 2.0 if len(data_points) % 2 > 0: result = (data_points[start] + data_points[start + 1]) / 2.0 else: result = self._round(data_points[start] // 2.0) return result def calc_sum(self, data_points): return self._round(sum(data_points)) def gather_data(self): """The main function that gathers all data and returns an updated configuration file that the index template can read""" infrastructure = self.conf['infrastructure'] for infra_conf in infrastructure: logging.info('Gather data from {}...'.format(infra_conf['title'])) devices = self._get_devices(infra_conf) for metric_conf in infra_conf['metrics']: metric = metric_conf.get('metrickey') cumulative = metric_conf.get('cumulative', True) # giving the option to show case static information in boxes if metric: multiplier = metric_conf.get('multiplier', 1) data_entries = self._get_metrics(metric, devices) for method in metric_conf['calculation']: data_points = self._get_data_points(cumulative, data_entries, multiplier) result = getattr(self, 'calc_{}'.format(method))(data_points) metric_conf['{}_stat'.format(method)] = result return self.conf def metric_filter(self, metrics, filter=None): """from a list of metrics ie ['cpuStats', 'CPUs', 'usr'] it constructs a dictionary that can be sent to the metrics endpoint for consumption""" metrics = list(metrics) if not filter: filter = {} filter[metrics.pop()] = 'all' return self.metric_filter(metrics, filter) else: try: metric = metrics.pop() dic = {metric: filter} return self.metric_filter(metrics, dic) except IndexError: return filter def available(self): """Assumes that all metrics are the same for a group or a tag""" infrastructure = self.conf['infrastructure'] md = '# Available metrics for all your groups\n\n' for infra_conf in infrastructure: if infra_conf.get('group'): category = infra_conf['group'] elif infra_conf.get('tag'): category = infra_conf['tags'] else: raise Exception('You need to provide either a group or tag') logging.info('Gathering metrics from {}...'.format(category)) devices = self._get_devices(infra_conf) device = devices[0] end = dt.datetime.now() start = end - timedelta(hours=2) available = self.metrics.available(device['_id'], start, end) metrics = self.flatten(available) try: md += '## {}\n'.format(infra_conf['title']) except KeyError: raise KeyError('Each section need a title, go on fill one in and try again.') for metric in metrics: title = ' '.join([tup[0] for tup in metric]) metric = '.'.join([tup[1] for tup in metric]) entry = '##### {}\nmetrickey: {}\n\n'.format(title, metric) md += entry with codecs.open('available.md', 'w') as f: f.write(md) def flatten(self, lst): """Get all the keys when calling available""" for dct in lst: key = dct['key'] name = dct['name'] if 'tree' not in dct: yield [(name, key)] # base case else: for result in self.flatten(dct["tree"]): # recursive case yield [(name, key)] + result
class Wrapper(BaseWrapper): def __init__(self, msg, server): super(Wrapper, self).__init__(msg, server) self.device = Device(self.token) self.metrics = Metrics(self.token) def results_of(self, command, metrics, name): if command == 'help' or name == 'help': result = self.extra_help(command) elif command == 'find': result = self.find_device(name) elif command == 'value': result = self.get_value(name, metrics) elif command == 'available': result = self.get_available(name) elif command == 'list': result = self.list_devices(name) return result def extra_help(self, command): help_command = { 'value': { 'title': 'Latest Value for a Device', 'mrkdwn_in': ['text'], 'text': ('To get the latest value for a device, type ' + '`sdbot devices metric.here for deviceName`. ' + 'The metrics need to be separated by dots.'), 'color': COLOR }, 'find': { 'title': 'Find a Device', 'mrkdwn_in': ['text'], 'text': ('To find a device type ' + '`sdbot devices find deviceName`. I can also accept regex for the argument `deviceName`. ' + 'For example `sdbot devices find 2$`.'), 'color': COLOR }, 'list': { 'title': 'List Devices', 'mrkdwn_in': ['text'], 'text': ('To get a list of your devices type, ' + '`sdbot devices list <no>`. In this case `<no>` ' + 'a number. If you leave it out I will ' + 'list the first 5 devices.'), 'color': COLOR }, 'available': { 'title': 'Available Metrics', 'mrkdwn_in': ['text'], 'text': ('To get all the available metrics for a device, type ' + '`sdbot devices available deviceName`. This will ' + 'display a list of metrics you can use for the command `devices value` or `graph`' ), 'color': COLOR } } if command == 'value': helptext = [help_command['value']] elif command == 'available': helptext = [help_command['available']] elif command == 'find': helptext = [help_command['find']] elif command == 'list': helptext = [help_command['list']] elif command == 'help': helptext = [attachment for attachment in help_command.values()] return helptext def _format_devices(self, devices): formatted = [{ 'text': '*Device Name*: {}'.format(device['name']), 'color': COLOR, 'mrkdwn_in': ['text'], 'fields': [{ 'title': 'Group', 'value': device.get('group') if device.get('group') else 'Ungrouped', 'short': True }, { 'title': 'Provider', 'value': device.get('provider') if device.get('provider') else 'No provider', 'short': True }, { 'title': 'Id', 'value': device.get('_id'), 'short': True }, { 'title': 'Online Status', 'value': self.online_status(device.get('lastPayloadAt', '')), 'short': True }] } for device in devices] return formatted def list_devices(self, number): if number: try: number = number.strip() number = int(number) except ValueError: text = '{} is not a number, now is it. You see, it needs to be.'.format( number) return text devices = self.device.list() if number: devices_trunc = devices[:number] else: devices_trunc = devices[:5] return self._format_devices(devices_trunc) def find_device(self, name): devices = self.device.list() if not name: msg = 'Here are all the devices that I found' device_list = "\n".join([device['name'] for device in devices]) result = msg + '\n```' + device_list + '```' return result devices = [ device for device in devices if re.search(name, device['name']) ] formatted_devices = self._format_devices(devices) if len(formatted_devices) == 0: formatted_devices = [{ 'text': 'Sorry, I couldn\'t find a device with that name :(', 'color': COLOR }] return formatted_devices def get_value(self, name, metrics): devices = self.device.list() _id = self.find_id(name, [], devices) if not _id: return 'I couldn\'t find your device' if not metrics: return ( 'You have not included any metrics the right way to do it ' + 'is give metrics this way `sdbot devices value memory.memSwapFree for {}`' .format(name)) metrics = metrics.split('.') _, filter = self.metric_filter(metrics) now = datetime.now() past30 = now - timedelta(minutes=35) metrics = self.metrics.get(_id, past30, now, filter) device, names = self.get_data(metrics) if not device.get('data'): return 'Could not find any data for these metrics' result = { 'title': 'Device name: {}'.format(name), 'text': ' > '.join(names), 'color': COLOR, 'fields': [{ 'title': 'Latest Value', 'value': '{}{}'.format(device['data'][-1]['y'], self.extract_unit(device)), 'short': True }] } return [result] def flatten(self, lst): for dct in lst: key = dct["key"] if "tree" not in dct: yield [key] # base case else: for result in self.flatten(dct["tree"]): # recursive case yield [key] + result def get_available(self, name): devices = self.device.list() _id = self.find_id(name, [], devices) if not _id: return 'It looks like there is no device named `{}`'.format(name) now = datetime.now() past30 = now - timedelta(minutes=120) metrics = self.metrics.available(_id, past30, now) available = list(self.flatten(metrics)) text = '' for a in available: text += '.'.join(a) + '\n' if text: text = 'Here are the metrics you can use\n' + '```' + text + '```' else: text = 'Your device seems to be offline, it doesn\'t contain any metrics in the last 2 hours' return text
class Wrapper(BaseWrapper): def __init__(self): super(Wrapper, self).__init__() self.device = Device(self.token) self.metrics = Metrics(self.token) def results_of(self, command, metrics, name): if command == 'help' or name == 'help': result = self.extra_help(command) elif command == 'find': result = self.find_device(name) elif command == 'value': result = self.get_value(name, metrics) elif command == 'available': result = self.get_available(name) elif command == 'list': result = self.list_devices(name) return result def extra_help(self, command): help_command = { 'value': { 'title': 'Latest Value for a Device', 'mrkdwn_in': ['text'], 'text': ('To get the latest value for a device, type ' + '`sdbot devices metric.here for deviceName`. ' + 'The metrics need to be separated by dots.'), 'color': COLOR }, 'find': { 'title': 'Find a Device', 'mrkdwn_in': ['text'], 'text': ('To find a device type ' + '`sdbot devices find deviceName`. I can also accept regex for the argument `deviceName`. ' + 'For example `sdbot devices find 2$`.'), 'color': COLOR }, 'list': { 'title': 'List Devices', 'mrkdwn_in': ['text'], 'text': ('To get a list of your devices type, ' + '`sdbot devices list <no>`. In this case `<no>` ' + 'a number. If you leave it out I will ' + 'list the first 5 devices.'), 'color': COLOR }, 'available': { 'title': 'Available Metrics', 'mrkdwn_in': ['text'], 'text': ('To get all the available metrics for a device, type ' + '`sdbot devices available deviceName`. This will ' + 'display a list of metrics you can use for the command `devices value` or `graph`'), 'color': COLOR } } if command == 'value': helptext = [help_command['value']] elif command == 'available': helptext = [help_command['available']] elif command == 'find': helptext = [help_command['find']] elif command == 'list': helptext = [help_command['list']] elif command == 'help': helptext = [attachment for attachment in help_command.values()] return helptext def _format_devices(self, devices): formatted = [{ 'text': '*Device Name*: {}'.format(device['name']), 'color': COLOR, 'mrkdwn_in': ['text'], 'fields': [{ 'title': 'Group', 'value': device.get('group') if device.get('group') else 'Ungrouped', 'short': True }, { 'title': 'Provider', 'value': device.get('provider') if device.get('provider') else 'No provider', 'short': True }, { 'title': 'Id', 'value': device.get('_id'), 'short': True }, { 'title': 'Online Status', 'value': self.online_status(device.get('lastPayloadAt', '')), 'short': True } ] } for device in devices] return formatted def list_devices(self, number): if number: try: number = number.strip() number = int(number) except ValueError: text = '{} is not a number, now is it. You see, it needs to be.'.format(number) return text devices = self.device.list() if number: devices_trunc = devices[:number] else: devices_trunc = devices[:5] return self._format_devices(devices_trunc) def find_device(self, name): devices = self.device.list() if not name: msg = 'Here are all the devices that I found' device_list = "\n".join([device['name'] for device in devices]) result = msg + '\n```' + device_list + '```' return result devices = [device for device in devices if re.search(name, device['name'])] formatted_devices = self._format_devices(devices) if len(formatted_devices) == 0: formatted_devices = [{ 'text': 'Sorry, I couldn\'t find a device with that name :(', 'color': COLOR }] return formatted_devices def get_value(self, name, metrics): devices = self.device.list() _id = self.find_id(name, [], devices) if not _id: return 'I couldn\'t find your device' if not metrics: return ('You have not included any metrics the right way to do it ' + 'is give metrics this way `sdbot devices value memory.memSwapFree for {}`'.format(name)) metrics = metrics.split('.') _, filter = self.metric_filter(metrics) now = datetime.now() past30 = now - timedelta(minutes=35) metrics = self.metrics.get(_id, past30, now, filter) device, names = self.get_data(metrics) if not device.get('data'): return 'Could not find any data for these metrics' result = { 'title': 'Device name: {}'.format(name), 'text': ' > '.join(names), 'color': COLOR, 'fields': [ { 'title': 'Latest Value', 'value': '{}{}'.format(device['data'][-1]['y'], self.extract_unit(device)), 'short': True } ] } return [result] def flatten(self, lst): for dct in lst: key = dct["key"] if "tree" not in dct: yield [key] # base case else: for result in self.flatten(dct["tree"]): # recursive case yield [key] + result def get_available(self, name): devices = self.device.list() _id = self.find_id(name, [], devices) if not _id: return 'It looks like there is no device named `{}`'.format(name) now = datetime.now() past30 = now - timedelta(minutes=120) metrics = self.metrics.available(_id, past30, now) available = list(self.flatten(metrics)) text = '' for a in available: text += '.'.join(a) + '\n' if text: text = 'Here are the metrics you can use\n' + '```' + text + '```' else: text = 'Your device seems to be offline, it doesn\'t contain any metrics in the last 2 hours' return text
class Wrapper(BaseWrapper): def __init__(self, msg, server): super(Wrapper, self).__init__() self.metrics = Metrics(self.token) self.device = Device(self.token) self.server = server self.msg = msg def results_of(self, metrics, name, period): if name == 'help': mod = importlib.import_module('limbo.plugins.graph') result = [json.loads(mod.__doc__)] else: result = self.get_metrics(metrics, name, period) return result def create_graph(self, device, difference): # 800 and 355 pixels. ticks = 5 width = 8 height = 3.55 dpi = 100 bgcolor = '#f3f6f6' font = {'size': 16, 'family': 'Arial'} plt.rc('font', **font) diff_sec = int(difference.total_seconds()) if diff_sec < 86400: x_no_ticks = 10 fmt = '%H:%M' elif (3600 * 24) < diff_sec and diff_sec < (3600 * 24 * 4): x_no_ticks = 5 fmt = '%d %b, %H:%M' elif (3600 * 24 * 4) < diff_sec: x_no_ticks = 6 fmt = '%d %b %Y' # size of figure and setting background color fig = plt.gcf() fig.set_size_inches(width, height) fig.set_facecolor(bgcolor) # axis color, no ticks and bottom line in grey color. ax = plt.axes(axisbg=bgcolor, frameon=True) ax.xaxis.set_ticks_position('none') ax.spines['bottom'].set_color('#aabcc2') ax.yaxis.set_ticks_position('none') # removing all but bottom spines for key, sp in ax.spines.items(): if key != 'bottom': sp.set_visible(False) # setting amounts of ticks on y axis yloc = plt.MaxNLocator(ticks) ax.yaxis.set_major_locator(yloc) # Deciding how many ticks we want on the graph locator = AutoDateLocator(minticks=(x_no_ticks - 2), maxticks=x_no_ticks) formatter = DateFormatter(fmt) ax.xaxis.set_major_locator(locator) ax.xaxis.set_major_formatter(formatter) # turns off small ticks plt.tick_params(axis='x', which='both', bottom='on', top='off', pad=10) # Can't seem to set label color differently, changing tick_params color changes labels. ax.xaxis.label.set_color('#FFFFFF') # setting dates in x-axis automatically triggers use of AutoDateLocator x = [datetime.fromtimestamp(point['x']) for point in device['data']] y = [point['y'] for point in device['data']] plt.plot(x, y, color='#53b4d4', linewidth=2) # pick values for y-axis y_ticks_values = np.array([point['y'] for point in device['data']]) y_ticks = np.linspace(y_ticks_values.min(), y_ticks_values.max(), ticks) y_ticks = np.round(y_ticks, decimals=2) plt.yticks(y_ticks, [str(val) + self.extract_unit(device) for val in y_ticks]) # plt.ylim(ymin=0.1) # Only show values of a certain threshold. plt.tight_layout() buf = io.BytesIO() plt.savefig(buf, format='png', facecolor=fig.get_facecolor(), dpi=dpi) buf.seek(0) filename = 'graph-{}.png'.format(int(time.time())) with io.open(filename, 'wb') as f: f.write(buf.read()) plt.close() return filename def get_metrics(self, metrics, name, period): devices = self.device.list() _id = self.find_id(name, [], devices) if not _id: return 'I couldn\'t find your device.' metrics_names = metrics.split('.') _, filter = self.metric_filter(metrics_names) cal = parsedatetime.Calendar() if not period: period = '2 hours ago' past, _ = cal.parseDT(datetimeString=period, tzinfo=self.timezone) now = datetime.now(self.timezone) if past > now: return 'Hey, I can\'t predict your data into the future, your date has to be in the past and now your date is {}'.format( past) metrics_data = self.metrics.get(_id, past, now, filter) device, names = self.get_data(metrics_data) if not device.get('data'): text = ( 'It might be that your device is offline or has no metrics for `{}`.' .format(metrics) + 'You can see what metrics are available by using `sdbot devices available {}`' .format(name)) return text # creates file slack = Slacker(os.environ.get('SLACK_TOKEN')) slack.chat.post_message( self.msg['channel'], 'Preparing the graphs for you this very moment', as_user=self.server.slack.server.username) filename = self.create_graph(device, now - past) attachment = [{ 'text': ('I brought you a graph for {} for the device `{}`'.format( ' '.join(names), name) + '\n A graph is coming up in just a sec.'), 'mrkdwn_in': ['text'], 'color': COLOR }] slack.chat.post_message(self.msg['channel'], '', attachments=attachment, as_user=self.server.slack.server.username) # uploads and sends the graph to channel file_response = slack.files.upload(filename, filename='{}.png'.format(name), channels=self.msg['channel']) # Deletes the graph file after upload so it doesn't litter the container os.remove(filename) return None # We're sending information in function itself this time
class Wrapper(BaseWrapper): def __init__(self, msg, server): super(Wrapper, self).__init__(msg, server) self.service = Service(self.token) self.metrics = Metrics(self.token) self.status = ServiceStatus(self.token) def results_of(self, command, name): if command == 'help' or name == 'help': result = self.extra_help(command) elif command == 'value': result = self.get_value(name) elif command == 'status': result = self.get_status(name) elif command == 'find': result = self.find_service(name) elif command == 'list': result = self.list_service(name) return result def extra_help(self, command): help_command = { 'status': { 'title': 'Overall Status', 'mrkdwn_in': ['text'], 'text': ('Overall Status displays statistics about your services. ' + 'It includes _Round trip time_, _Response Time_, ' + '_Status Code_ and _Status of location_. To get ' + 'the status of a service you can type `sdbot services status serviceName`'), 'color': COLOR }, 'find': { 'title': 'Find a Service', 'mrkdwn_in': ['text'], 'text': ('You can find a service by typing `sdbot services find serviceName`. ' + 'I can also accept regex for the argument `serviceName`. ' + 'For example `sdbot services find 2$`.'), 'color': COLOR }, 'list': { 'title': 'List All Services', 'mrkdwn_in': ['text'], 'text': ('For a list of all services, type `sdbot services list <no>`.' + 'In this case `<no>` is a number. If you leave it out I will ' + 'list the first 5 services.'), 'color': COLOR } } if command == 'status': helptext = [help_command['status']] elif command == 'find': helptext = [help_command['find']] elif command == 'list': helptext = [help_command['list']] elif command == 'help': helptext = [attachment for attachment in help_command.values()] return helptext, '' def _format_services(self, http, tcp): slack_http = [{ 'text': '*Service Name*: {}'.format(service['name']), 'color': COLOR, 'mrkdwn_in': ['text'], 'fields': [{ 'title': 'Group', 'value': service.get('group') if service.get('group') else 'Ungrouped', 'short': True }, { 'title': 'Type of check', 'value': service.get('checkType'), 'short': True }, { 'title': 'Url', 'value': service.get('checkUrl'), 'short': True }, { 'title': 'Method', 'value': service.get('checkMethod'), 'short': True }, { 'title': 'Slow threshold', 'value': str(service.get('slowThreshold')) + 'ms', 'short': True } ] } for service in http] slack_tcp = [{ 'text': '*Service Name*: {}'.format(service['name']), 'color': COLOR, 'mrkdwn_in': ['text'], 'fields': [{ 'title': 'Group', 'value': service.get('group') if service.get('group') else 'Ungrouped', 'short': True }, { 'title': 'Type of check', 'value': service.get('checkType'), 'short': True }, { 'title': 'Host', 'value': service.get('host'), 'short': True }, { 'title': 'Port', 'value': service.get('port'), 'short': True } ] } for service in tcp] return slack_http + slack_tcp def list_service(self, number): if number: try: number = number.strip() number = int(number) except ValueError: text = '{} is not a number, now is it. You see, it needs to be.'.format(number) return text, '' services = self.service.list() if number: services_trunc = services[:number] else: services_trunc = services[:5] http = [s for s in services_trunc if s['checkType'] == 'http'] tcp = [s for s in services_trunc if s['checkType'] == 'tcp'] formatted = self._format_services(http, tcp) message = ('You have {} services, if you would like to'.format(len(services)) + ' list more than these {} services, use '.format(len(services_trunc)) + '`sdbot services list <no>`') return formatted, message def find_service(self, name): services = self.service.list() http = [s for s in services if s['checkType'] == 'http' and re.search(name, s['name'])] tcp = [s for s in services if s['checkType'] == 'tcp' and re.search(name, s['name'])] return self._format_services(http, tcp), '' def get_value(self, name): services = self.service.list() _id = self.find_id(name, services, []) if not _id: return 'I couldn\'t find your service', '' service = self.service.view(_id) locations = service['checkLocations'] all_results = [] for location in locations: filtered = {'time': {location: 'all'}} now = datetime.now() past30 = now - timedelta(minutes=35) metrics = self.metrics.get(_id, past30, now, filtered) service = metrics[0]['tree'][0] data = service['data'] try: latest = '{}s'.format(round(data[-1]['y'], 3)) avg = '{}s'.format(round(sum([point['y'] for point in data])/len(data), 3)) except IndexError: latest = 'down' avg = 'down' result = { 'title': service['name'], 'color': COLOR, 'fields': [ { 'title': '30 Minute Average', 'value': avg, 'short': True }, { 'title': 'Latest Value', 'value': latest, 'short': True } ] } all_results.append(result) message = 'Here is the latest values for the {} locations of the service {}'.format(len(locations), name) return all_results, message def real_name(self, _id, nodes): for node in nodes: if _id == node['id']: return node['name'] def get_status(self, name): services = self.service.list() _id = self.find_id(name, services, []) if not _id: return 'I couldn\'t find your service', '' nodes = requests.get(BASEURL + 'service-monitor/nodes', params={'token': self.token}) statuses = self.status.location(_id) all_results = [] for status in statuses: result = { 'title': self.real_name(status['location'], nodes.json()), 'color': COLOR, 'fields': [ { 'title': 'Round Trip Time', 'value': '{}s'.format(round(float(status.get('rtt', 0)), 3)), 'short': True }, { 'title': 'Status of Location', 'value': status['status'], 'short': True }, { 'title': 'Response Time', 'value': '{}s'.format(round(float(status.get('time', 0)), 3)), 'short': True }, { 'title': 'Status Code', 'value': status['code'], 'short': True } ] } all_results.append(result) message = 'This is the status of all your locations for the service {}'.format(name) return all_results, message
class Wrapper(BaseWrapper): def __init__(self, msg, server): super(Wrapper, self).__init__() self.service = Service(self.token) self.metrics = Metrics(self.token) self.status = ServiceStatus(self.token) self.server = server self.msg = msg def results_of(self, command, name): if command == 'help' or name == 'help': result = self.extra_help(command) elif command == 'value': result = self.get_value(name) elif command == 'status': result = self.get_status(name) elif command == 'find': result = self.find_service(name) elif command == 'list': result = self.list_service(name) return result def extra_help(self, command): help_command = { 'status': { 'title': 'Overall Status', 'mrkdwn_in': ['text'], 'text': ('Overall Status displays statistics about your services. ' + 'It includes _Round trip time_, _Response Time_, ' + '_Status Code_ and _Status of location_. To get ' + 'the status of a service you can type `sdbot services status serviceName`' ), 'color': COLOR }, 'find': { 'title': 'Find a Service', 'mrkdwn_in': ['text'], 'text': ('You can find a service by typing `sdbot services find serviceName`. ' + 'I can also accept regex for the argument `serviceName`. ' + 'For example `sdbot services find 2$`.'), 'color': COLOR }, 'list': { 'title': 'List All Services', 'mrkdwn_in': ['text'], 'text': ('For a list of all services, type `sdbot services list <no>`.' + 'In this case `<no>` is a number. If you leave it out I will ' + 'list the first 5 services.'), 'color': COLOR } } if command == 'status': helptext = [help_command['status']] elif command == 'find': helptext = [help_command['find']] elif command == 'list': helptext = [help_command['list']] elif command == 'help': helptext = [attachment for attachment in help_command.values()] return helptext, '' def _format_services(self, http, tcp): slack_http = [{ 'text': '*Service Name*: {}'.format(service['name']), 'color': COLOR, 'mrkdwn_in': ['text'], 'fields': [{ 'title': 'Group', 'value': service.get('group') if service.get('group') else 'Ungrouped', 'short': True }, { 'title': 'Type of check', 'value': service.get('checkType'), 'short': True }, { 'title': 'Url', 'value': service.get('checkUrl'), 'short': True }, { 'title': 'Method', 'value': service.get('checkMethod'), 'short': True }, { 'title': 'Slow threshold', 'value': str(service.get('slowThreshold')) + 'ms', 'short': True }] } for service in http] slack_tcp = [{ 'text': '*Service Name*: {}'.format(service['name']), 'color': COLOR, 'mrkdwn_in': ['text'], 'fields': [{ 'title': 'Group', 'value': service.get('group') if service.get('group') else 'Ungrouped', 'short': True }, { 'title': 'Type of check', 'value': service.get('checkType'), 'short': True }, { 'title': 'Host', 'value': service.get('host'), 'short': True }, { 'title': 'Port', 'value': service.get('port'), 'short': True }] } for service in tcp] return slack_http + slack_tcp def list_service(self, number): if number: try: number = number.strip() number = int(number) except ValueError: text = '{} is not a number, now is it. You see, it needs to be.'.format( number) return text, '' services = self.service.list() if number: services_trunc = services[:number] else: services_trunc = services[:5] http = [s for s in services_trunc if s['checkType'] == 'http'] tcp = [s for s in services_trunc if s['checkType'] == 'tcp'] formatted = self._format_services(http, tcp) message = ('You have {} services, if you would like to'.format( len(services)) + ' list more than these {} services, use '.format( len(services_trunc)) + '`sdbot services list <no>`') return formatted, message def find_service(self, name): services = self.service.list() http = [ s for s in services if s['checkType'] == 'http' and re.search(name, s['name']) ] tcp = [ s for s in services if s['checkType'] == 'tcp' and re.search(name, s['name']) ] return self._format_services(http, tcp), '' def get_value(self, name): services = self.service.list() _id = self.find_id(name, services, []) if not _id: return 'I couldn\'t find your service', '' service = self.service.view(_id) locations = service['checkLocations'] all_results = [] for location in locations: filtered = {'time': {location: 'all'}} now = datetime.now() past30 = now - timedelta(minutes=35) metrics = self.metrics.get(_id, past30, now, filtered) service = metrics[0]['tree'][0] data = service['data'] try: latest = '{}s'.format(round(data[-1]['y'], 3)) avg = '{}s'.format( round(sum([point['y'] for point in data]) / len(data), 3)) except IndexError: latest = 'down' avg = 'down' result = { 'title': service['name'], 'color': COLOR, 'fields': [{ 'title': '30 Minute Average', 'value': avg, 'short': True }, { 'title': 'Latest Value', 'value': latest, 'short': True }] } all_results.append(result) message = 'Here is the latest values for the {} locations of the service {}'.format( len(locations), name) return all_results, message def real_name(self, _id, nodes): for node in nodes: if _id == node['id']: return node['name'] def get_status(self, name): services = self.service.list() _id = self.find_id(name, services, []) if not _id: return 'I couldn\'t find your service', '' nodes = requests.get(BASEURL + 'service-monitor/nodes', params={'token': self.token}) statuses = self.status.location(_id) all_results = [] for status in statuses: result = { 'title': self.real_name(status['location'], nodes.json()), 'color': COLOR, 'fields': [{ 'title': 'Round Trip Time', 'value': '{}s'.format(round(float(status.get('rtt', 0)), 3)), 'short': True }, { 'title': 'Status of Location', 'value': status['status'], 'short': True }, { 'title': 'Response Time', 'value': '{}s'.format(round(float(status.get('time', 0)), 3)), 'short': True }, { 'title': 'Status Code', 'value': status['code'], 'short': True }] } all_results.append(result) message = 'This is the status of all your locations for the service {}'.format( name) return all_results, message
class Wrapper(BaseWrapper): def __init__(self, msg, server): super(Wrapper, self).__init__() self.metrics = Metrics(self.token) self.device = Device(self.token) self.server = server self.msg = msg def results_of(self, metrics, name, period): if name == 'help': mod = importlib.import_module('limbo.plugins.graph') result = [json.loads(mod.__doc__)] else: result = self.get_metrics(metrics, name, period) return result def create_graph(self, device, difference): # 800 and 355 pixels. ticks = 5 width = 8 height = 3.55 dpi = 100 bgcolor = '#f3f6f6' font = { 'size': 16, 'family': 'Arial' } plt.rc('font', **font) diff_sec = int(difference.total_seconds()) if diff_sec < 86400: x_no_ticks = 10 fmt = '%H:%M' elif (3600*24) < diff_sec and diff_sec < (3600*24*4): x_no_ticks = 5 fmt = '%d %b, %H:%M' elif (3600*24*4) < diff_sec: x_no_ticks = 6 fmt = '%d %b %Y' # size of figure and setting background color fig = plt.gcf() fig.set_size_inches(width, height) fig.set_facecolor(bgcolor) # axis color, no ticks and bottom line in grey color. ax = plt.axes(axisbg=bgcolor, frameon=True) ax.xaxis.set_ticks_position('none') ax.spines['bottom'].set_color('#aabcc2') ax.yaxis.set_ticks_position('none') # removing all but bottom spines for key, sp in ax.spines.items(): if key != 'bottom': sp.set_visible(False) # setting amounts of ticks on y axis yloc = plt.MaxNLocator(ticks) ax.yaxis.set_major_locator(yloc) # Deciding how many ticks we want on the graph locator = AutoDateLocator(minticks=(x_no_ticks - 2), maxticks=x_no_ticks) formatter = DateFormatter(fmt) ax.xaxis.set_major_locator(locator) ax.xaxis.set_major_formatter(formatter) # turns off small ticks plt.tick_params(axis='x', which='both', bottom='on', top='off', pad=10) # Can't seem to set label color differently, changing tick_params color changes labels. ax.xaxis.label.set_color('#FFFFFF') # setting dates in x-axis automatically triggers use of AutoDateLocator x = [datetime.fromtimestamp(point['x']) for point in device['data']] y = [point['y'] for point in device['data']] plt.plot(x, y, color='#53b4d4', linewidth=2) # pick values for y-axis y_ticks_values = np.array([point['y'] for point in device['data']]) y_ticks = np.linspace(y_ticks_values.min(), y_ticks_values.max(), ticks) y_ticks = np.round(y_ticks, decimals=2) plt.yticks(y_ticks, [str(val) + self.extract_unit(device) for val in y_ticks]) # plt.ylim(ymin=0.1) # Only show values of a certain threshold. plt.tight_layout() buf = io.BytesIO() plt.savefig(buf, format='png', facecolor=fig.get_facecolor(), dpi=dpi) buf.seek(0) filename = 'graph-{}.png'.format(int(time.time())) with io.open(filename, 'wb') as f: f.write(buf.read()) plt.close() return filename def get_metrics(self, metrics, name, period): devices = self.device.list() _id = self.find_id(name, [], devices) if not _id: return 'I couldn\'t find your device.' metrics_names = metrics.split('.') _, filter = self.metric_filter(metrics_names) cal = parsedatetime.Calendar() if not period: period = '2 hours ago' past, _ = cal.parseDT(datetimeString=period, tzinfo=self.timezone) now = datetime.now(self.timezone) if past > now: return 'Hey, I can\'t predict your data into the future, your date has to be in the past and now your date is {}'.format(past) metrics_data = self.metrics.get(_id, past, now, filter) device, names = self.get_data(metrics_data) if not device.get('data'): text = ('It might be that your device is offline or has no metrics for `{}`.'.format(metrics) + 'You can see what metrics are available by using `sdbot devices available {}`'.format(name)) return text # creates file slack = Slacker(os.environ.get('SLACK_TOKEN')) slack.chat.post_message( self.msg['channel'], 'Preparing the graphs for you this very moment', as_user=self.server.slack.server.username ) filename = self.create_graph(device, now - past) attachment = [ { 'text': ('I brought you a graph for {} for the device `{}`'.format(' '.join(names), name) + '\n A graph is coming up in just a sec.'), 'mrkdwn_in': ['text'], 'color': COLOR } ] slack.chat.post_message( self.msg['channel'], '', attachments=attachment, as_user=self.server.slack.server.username ) # uploads and sends the graph to channel file_response = slack.files.upload( filename, filename='{}.png'.format(name), channels=self.msg['channel'] ) # Deletes the graph file after upload so it doesn't litter the container os.remove(filename) return None # We're sending information in function itself this time
class DataWrapper(object): def __init__(self, token, conf): self.token = token self.conf = conf self.historic_data = None client = ApiClient(self.token, timeout=(3.05, 18)) self.device = Device(self.token) self.tag = Tag(self.token) self.metrics = Metrics(client) self._validation() def _validation(self): """Check the following - metric for every piece in infrastructure - That the configuration contains everything necessary. - check that there are no none values in any lists. """ def _get_devices(self, infra_conf): """Takes the configuration part for each infrastructure""" raw_devices = self.device.list() if infra_conf.get('tag'): tags = self.tag.list() tag_id = [ tag['_id'] for tag in tags if tag['name'] == infra_conf['tag'] ] if not tag_id: available_tags = '\n'.join( list(set([tag['name'] for tag in tags]))) message = 'There is no tag with the name "{}". Try one of these: \n{}'.format( infra_conf['tag'], available_tags) raise Exception(message) else: tag_id = tag_id[0] devices = [ device for device in raw_devices if tag_id in device.get('tags', []) ] if not devices: available_tags = '\n'.join( list(set([tag['name'] for tag in tags]))) raise Exception( 'There is no device with this tag name. Try one of these: \n{}' .format(available_tags)) elif infra_conf.get('group'): group = infra_conf.get('group') devices = [ device for device in raw_devices if group == device.get('group') ] if not devices: groups = set([ device['group'] for device in raw_devices if not device['group'] is None ]) groups = '\n'.join(list(groups)) raise Exception( 'There is no device with this group name. The following groups are available:\n {}' .format(groups)) else: raise Exception( 'You have to provide either group or tag for each part of your infrastructure.' ) return devices def _merge_loadbalanced_data(self, data_points, points): max_length = len(data_points) for i, point in enumerate(points): if i < max_length: data_points[i] = data_points[i] + point return data_points def _get_metrics(self, metric, devices): """For all devices associated with the group or device""" metric = metric.split('.') metric_filter = self.metric_filter(metric) end = dt.datetime.now() start = end - timedelta( hours=self.conf['general'].get('timeframe', 24)) data_entries = [] for device in devices: data = self.metrics.get(device['_id'], start, end, metric_filter) data = self._data_node(data) time.sleep(0.3) if data['data']: data_entries.append(data) if not data_entries: metric = '.'.join(metric) # Append zero data to avoid zerodivison error data_entries.append({'data': [{'x': 0, 'y': 0}]}) logging.warning( 'No server in this group has any data on {}'.format(metric)) return data_entries def _data_node(self, data, names=None): """Inputs the data from the metrics endpoints and returns the node that has contains the data + names of the metrics.""" if not names: names = [] for d in data: if d.get('data') or d.get('data') == []: names.append(d.get('name')) d['full_name'] = names return d else: names.append(d.get('name')) return self._data_node(d.get('tree'), names) def _get_data_points(self, cumulative, data_entries, multiplier): """Extract the singular points into a list and return a list of those points""" data_points = [] for data in data_entries: points = [point['y'] * multiplier for point in data['data']] if cumulative and len(data_points) > 0: self._merge_loadbalanced_data(data_points, points) else: data_points.extend(points) return data_points def _round(self, number): rounding = self.conf['general'].get('round', 2) if rounding > 0: return round(number, rounding) else: return int(round(number, 0)) def calc_average(self, data_points): return self._round(sum(data_points) / len(data_points)) def calc_max(self, data_points): return self._round(max(data_points)) def calc_min(self, data_points): return self._round(min(data_points)) def calc_median(self, data_points): data_points.sort() start = len(data_points) // 2.0 if len(data_points) % 2 > 0: result = (data_points[start] + data_points[start + 1]) / 2.0 else: result = self._round(data_points[start] // 2.0) return result def calc_sum(self, data_points): return self._round(sum(data_points)) def gather_data(self): """The main function that gathers all data and returns an updated configuration file that the index template can read""" infrastructure = self.conf['infrastructure'] for infra_conf in infrastructure: logging.info('Gather data from {}...'.format(infra_conf['title'])) devices = self._get_devices(infra_conf) for metric_conf in infra_conf['metrics']: metric = metric_conf.get('metrickey') cumulative = metric_conf.get('cumulative', True) # giving the option to show case static information in boxes if metric: multiplier = metric_conf.get('multiplier', 1) data_entries = self._get_metrics(metric, devices) for method in metric_conf['calculation']: data_points = self._get_data_points( cumulative, data_entries, multiplier) result = getattr(self, 'calc_{}'.format(method))(data_points) metric_conf['{}_stat'.format(method)] = result return self.conf def metric_filter(self, metrics, filter=None): """from a list of metrics ie ['cpuStats', 'CPUs', 'usr'] it constructs a dictionary that can be sent to the metrics endpoint for consumption""" metrics = list(metrics) if not filter: filter = {} filter[metrics.pop()] = 'all' return self.metric_filter(metrics, filter) else: try: metric = metrics.pop() dic = {metric: filter} return self.metric_filter(metrics, dic) except IndexError: return filter def available(self): """Assumes that all metrics are the same for a group or a tag""" infrastructure = self.conf['infrastructure'] md = '# Available metrics for all your groups\n\n' for infra_conf in infrastructure: if infra_conf.get('group'): category = infra_conf['group'] elif infra_conf.get('tag'): category = infra_conf['tags'] else: raise Exception('You need to provide either a group or tag') logging.info('Gathering metrics from {}...'.format(category)) devices = self._get_devices(infra_conf) device = devices[0] end = dt.datetime.now() start = end - timedelta(hours=2) available = self.metrics.available(device['_id'], start, end) metrics = self.flatten(available) try: md += '## {}\n'.format(infra_conf['title']) except KeyError: raise KeyError( 'Each section need a title, go on fill one in and try again.' ) for metric in metrics: title = ' '.join([tup[0] for tup in metric]) metric = '.'.join([tup[1] for tup in metric]) entry = '##### {}\nmetrickey: {}\n\n'.format(title, metric) md += entry with codecs.open('available.md', 'w') as f: f.write(md) def flatten(self, lst): """Get all the keys when calling available""" for dct in lst: key = dct['key'] name = dct['name'] if 'tree' not in dct: yield [(name, key)] # base case else: for result in self.flatten(dct["tree"]): # recursive case yield [(name, key)] + result