Source code for alignak.stats

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015-2015: Alignak team, see AUTHORS.txt file for contributors
#
# This file is part of Alignak.
#
# Alignak is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Alignak is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Alignak.  If not, see <http://www.gnu.org/licenses/>.
#
#
# This file incorporates work covered by the following copyright and
# permission notice:
#
#  Copyright (C) 2009-2014:
#     Grégory Starck, g.starck@gmail.com
#     Olivier Hanesse, olivier.hanesse@gmail.com
#     Jean Gabes, naparuba@gmail.com
#     Sebastien Coavoux, s.coavoux@free.fr

#  This file is part of Shinken.
#
#  Shinken is free software: you can redistribute it and/or modify
#  it under the terms of the GNU Affero General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  Shinken is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU Affero General Public License for more details.
#
#  You should have received a copy of the GNU Affero General Public License
#  along with Shinken.  If not, see <http://www.gnu.org/licenses/>.
"""This module provide export of Alignak metrics in a statsd format

"""
import threading
import time
import json
import hashlib
import base64
import socket

from alignak.log import logger
from alignak.http.client import HTTPClient, HTTPException


BLOCK_SIZE = 16


[docs]def pad(data): """Add data to fit BLOCK_SIZE :param data: initial data :return: data padded to fit BLOCK_SIZE """ pad = BLOCK_SIZE - len(data) % BLOCK_SIZE return data + pad * chr(pad)
[docs]def unpad(padded): """Unpad data based on last char :param padded: padded data :return: unpadded data """ pad = ord(padded[-1]) return padded[:-pad]
[docs]class Stats(object): """Stats class to export data into a statsd format """ def __init__(self): self.name = '' self.type = '' self.app = None self.stats = {} # There are two modes that are not exclusive # first the kernel mode self.api_key = '' self.secret = '' self.http_proxy = '' self.con = HTTPClient(uri='http://kernel.alignak.io') # then the statsd one self.statsd_sock = None self.statsd_addr = None
[docs] def launch_reaper_thread(self): """Launch thread that collects data :return: None """ self.reaper_thread = threading.Thread(None, target=self.reaper, name='stats-reaper') self.reaper_thread.daemon = True self.reaper_thread.start()
[docs] def register(self, app, name, _type, api_key='', secret='', http_proxy='', statsd_host='localhost', statsd_port=8125, statsd_prefix='alignak', statsd_enabled=False): """Init statsd instance with real values :param app: application (arbiter, scheduler..) :type app: alignak.daemon.Daemon :param name: daemon name :type name: str :param _type: daemon type :type _type: :param api_key: api_key to post data :type api_key: str :param secret: secret to post data :type secret: str :param http_proxy: proxy http if necessary :type http_proxy: str :param statsd_host: host to post data :type statsd_host: str :param statsd_port: port to post data :type statsd_port: int :param statsd_prefix: prefix to add to metric :type statsd_prefix: str :param statsd_enabled: bool to enable statsd :type statsd_enabled: bool :return: None """ self.app = app self.name = name self.type = _type # kernel.io part self.api_key = api_key self.secret = secret self.http_proxy = http_proxy # local statsd part self.statsd_host = statsd_host self.statsd_port = statsd_port self.statsd_prefix = statsd_prefix self.statsd_enabled = statsd_enabled if self.statsd_enabled: logger.debug('Loading statsd communication with %s:%s.%s', self.statsd_host, self.statsd_port, self.statsd_prefix) self.load_statsd() # Also load the proxy if need self.con.set_proxy(self.http_proxy)
[docs] def load_statsd(self): """Create socket connection to statsd host :return: None """ try: self.statsd_addr = (socket.gethostbyname(self.statsd_host), self.statsd_port) self.statsd_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) except (socket.error, socket.gaierror), exp: logger.error('Cannot create statsd socket: %s', exp) return
[docs] def incr(self, key, value): """Increments a key with value :param key: key to edit :type key: str :param value: value to add :type v: int :return: None """ _min, _max, number, _sum = self.stats.get(key, (None, None, 0, 0)) number += 1 _sum += value if _min is None or value < _min: _min = value if _max is None or value > _max: _max = value self.stats[key] = (_min, _max, number, _sum) # Manage local statd part if self.statsd_sock and self.name: # beware, we are sending ms here, value is in s packet = '%s.%s.%s:%d|ms' % (self.statsd_prefix, self.name, key, value * 1000) try: self.statsd_sock.sendto(packet, self.statsd_addr) except (socket.error, socket.gaierror), exp: pass # cannot send? ok not a huge problem here and cannot # log because it will be far too verbose :p
def _encrypt(self, data): """Cypher data :param data: data to cypher :type data: str :return: cyphered data :rtype: str """ md_hash = hashlib.md5() md_hash.update(self.secret) key = md_hash.hexdigest() md_hash = hashlib.md5() md_hash.update(self.secret + key) ivs = md_hash.hexdigest() data = pad(data) aes = AES.new(key, AES.MODE_CBC, ivs[:16]) encrypted = aes.encrypt(data) return base64.urlsafe_b64encode(encrypted)
[docs] def reaper(self): """Get data from daemon and send it to the statsd daemon :return: None """ try: from Crypto.Cipher import AES except ImportError: logger.error('Cannot find python lib crypto: stats export is not available') AES = None # pylint: disable=C0103 while True: now = int(time.time()) stats = self.stats self.stats = {} if len(stats) != 0: string = ', '.join(['%s:%s' % (key, v) for (key, v) in stats.iteritems()]) # If we are not in an initializer daemon we skip, we cannot have a real name, it sucks # to find the data after this if not self.name or not self.api_key or not self.secret: time.sleep(60) continue metrics = [] for (key, elem) in stats.iteritems(): namekey = '%s.%s.%s' % (self.type, self.name, key) _min, _max, number, _sum = elem _avg = float(_sum) / number # nb can't be 0 here and _min_max can't be None too string = '%s.avg %f %d' % (namekey, _avg, now) metrics.append(string) string = '%s.min %f %d' % (namekey, _min, now) metrics.append(string) string = '%s.max %f %d' % (namekey, _max, now) metrics.append(string) string = '%s.count %f %d' % (namekey, number, now) metrics.append(string) # logger.debug('REAPER metrics to send %s (%d)' % (metrics, len(str(metrics))) ) # get the inner data for the daemon struct = self.app.get_stats_struct() struct['metrics'].extend(metrics) # logger.debug('REAPER whole struct %s' % struct) j = json.dumps(struct) if AES is not None and self.secret != '': logger.debug('Stats PUT to kernel.alignak.io/api/v1/put/ with %s %s', self.api_key, self.secret) # assume a %16 length messagexs encrypted_text = self._encrypt(j) try: self.con.put('/api/v1/put/?api_key=%s' % (self.api_key), encrypted_text) except HTTPException, exp: logger.error('Stats REAPER cannot put to the metric server %s', exp) time.sleep(60) # pylint: disable=C0103
statsmgr = Stats()