URL: https://linuxfr.org/news/presentation-technique-de-canopsis Title: Présentation technique de Canopsis Authors: David Delassus Benoît Sibaud, Nÿco, palm123, bubar🦥 et claudex Date: 2016年02月05日T16:36:24+01:00 License: CC By-SA Tags: supervision, mongodb, canopsis, hypervision, monitoring, python et javascript Score: 16 Supervision et visualisation des données : des domaines de l'informatique qui nous permettent de surveiller, analyser une infra, des données afin de comprendre et éventuellement prédire des dysfonctionnements. De nombreux outils existent et excellent dans chacune de ces tâches. Les accorder ensemble afin d'unifier l'information permet ainsi de faciliter la prise de décision. C'est ce que l'on appelle **l'hypervision**. Canopsis se veut une solution d'hypervision (on évite l'appellation *hyperviseur* qui reste dans le langage courant spécifique à la virtualisation). Solution open-source sous licence AGPL3 développée par la société française [Capensis](http://capensis.fr), elle se veut simple d'utilisation, et suffisamment souple pour répondre à un maximum de besoin. ---- [Gitlab de Canopsis](https://git.canopsis.net/explore) [Capensis](https://capensis.fr) [Présentation d'un hyperviseur](http://www-igm.univ-mlv.fr/~dr/XPOSE2010/Hypervision/Hyperviseur.html) [Hypervision - Piloter votre supervision](http://www.informatique-securite.net/hypervision-supervision/) [Hypervision sur monitoring-fr](http://wiki.monitoring-fr.org/hypervision/start) ---- # La problématique Dans une infra hétérogène, on dispose de nombreuses sources d'information : * côté supervision, on peut avoir un mélange de : * Nagios/Icinga * Centreon * Shinken * HPOV * Zabbix * SNMP * ... * côté récolte de données, on peut avoir : * CollectD * Logstash * Munin * Telegraf (de la pile [TICK](https://influxdata.com/)) * ... * ou encore : * des logs * des données stockées en base de données * un résultat de tests unitaires et fonctionnels (jMeter, Sikuli...) La mise en place d'un accès à l'ensemble de ces informations peut être fastidieuse, et dans la plupart des cas, l'utilisateur (l'administrateur technique et/ou fonctionnel) devra accéder à plusieurs interfaces et maîtriser plusieurs outils. Cela empêche d'avoir une vue d'ensemble cohérente et rend difficile l'anticipation ainsi que la prise de décision. # Objectif : la solution C'est là que Canopsis intervient. ![Schéma fonctionnel](http://www.canopsis.com/wp-content/themes/canopsis/images/schema.png) ## Étape 1 : les connecteurs Dans un premier temps, nous devons récupérer les informations produites par cet ensemble hétérogène. Ainsi, pour chaque source de données, nous sommes en mesure de développer un **connecteur**, capable d'extraire les informations voulues, et de les transmettre à Canopsis sous la forme **d'événements** standardisés. Un certain nombre de connecteurs sont déjà disponibles sur le [Gitlab](https://git.canopsis.net/groups/canopsis-connectors). Le cas le plus simple est donc celui ci : ```python #!/usr/bin/env python from time import time # lib qui nous permet de nous connecter au bus de données (cf étape 2) from kombu import Connection # module qui sera utilisé pour envoyer les événements from kombu.pools import producers # construction de l'événement selon le standard Canopsis event = { "timestamp": int(time()), # émetteur de l'événement "connector": "myconnector", "connector_name": "myconnector-instance0", # nature de l'événement "event_type": "check", # source de l'événement "source_type": "resource", "component": "", "resource": "", # données portées par l'événement "state": 0, # 0 = INFO, 1 = MINOR, 2 = MAJOR, 3 = CRITICAL "output": "" } # construction de la routing_key, qui sert à identifier l'événement et à le router routing_key = "{0}.{1}.{2}.{3}.{4}".format( event['connector'], event['connector_name'], event['event_type'], event['source_type'], event['component'] ) if event['source_type'] == "resource": routing_key = "{0}.{1}".format( routing_key, event['resource'] ) # Connexion uri = 'amqp://cpsrabbit:canopsis@localhost:5672/canopsis' with Connection(uri) as conn: # Création de l'émetteur with producers[conn].acquire(block=True) as producer: # Publication producer.publish( event, serializer='json', exchange='canopsis.events', routing_key=routing_key ) ``` Bien évidemment, du moment qu'un connecteur envoie ses événements, il peut être développé dans n'importe quel langage, c'est le cas du connecteur Nagios qui se présente comme un Nagios Event Broker, et est donc développé en C. ## Étape 2 : le bus de données et les moteurs Les événements produits par les connecteurs sont transmis au bus de données de Canopsis, basé sur [RabbitMQ](https://www.rabbitmq.com/). ![Schema RabbitMQ](http://igm.univ-mlv.fr/~dr/XPOSE2011/rabbitmq/load_balancing.png) Source : http://igm.univ-mlv.fr/~dr/XPOSE2011/rabbitmq/usages.html Ces événements vont être consommés par des *daemons* que nous appelons **moteurs**. Leur but est simple : * traiter la donnée * enregistrer les informations pertinentes en base de données * transmettre, si nécessaire, l'événement à un ou plusieurs autres moteurs Nous avons, par exemple : * le moteur ``event_filter`` qui se charge de filtrer/modifier les événements entrants * le moteur ``context`` qui se charge d'enregistrer les informations sur l'entité cible de l'événement (connecteur source, composant/ressource qui a produit l'événement...) * le moteur ``perfdata`` qui s'occupe d'historiser les données de performance (comme remontées par Nagios ou CollectD) * ... Comme pour les *connecteurs*, les *moteurs* peuvent être développés dans n'importe quel langage, bien que pour le moment nous les avons tous faits en Python. Étant un *daemon*, un *moteur* dispose de 2 fonctions : * une pour consommer les événements (dans un *thread* à part) * une pour exécuter une tâche régulièrement (recharger la configuration, envoyer des stats...) Ainsi que la configuration suivante : ```ini [engine:myengine] # chemin Python de la méthode de consommation event_processing=canopsis.myfeature.process.event_processing # chemin Python de la méthode exécutée régulièrement beat_processing=canopsis.myfeature.process.beat_processing # nom de l'exchange AMQP sur lequel écouter (par défaut: amq.direct) exchange_name=canopsis.event # RK à consommer routing_keys=nagios.#,shinken.# # intervalle entre 2 exécutions du beat_processing (en secondes) beat_interval=60 # liste des moteurs sur lesquels retransmettre l'événement reçu, possiblement modifié (par défaut: aucun) next=myengine2,myengine3 ``` Et donc l'implémentation se résume à : ```python def event_processing(engine, event, **_): # traiter l'événement return event def beat_processing(engine, **_): # faire des choses ``` ## Étape 3 : les schémas Toutes les données qui véhiculent dans le bus et qui sont sauvegardées en base sont munies de schémas les décrivant. Ces schémas servent à plusieurs choses : * valider que la donnée est bien formatée * décrire comment la donnée sera représentée * décrire comment la donnée sera éditée Ces deux derniers points permettent de générer une partie du code de l'UI (cf étape 5). À terme, ils serviront également à : * décrire comment la donnée sera utilisée * décrire comment la donnée pourra être transformée Ce qui permettra de générer une partie du code backend (cf étape 4). Le formalisme qui permet d'écrire un schéma est actuellement inspiré du standard [JSON Schema](http://json-schema.org/) : ```javascript { "title": "MyData", "description": "Schéma décrivant la donnée, comment l'afficher et l'éditer" // description de la donnée "type": "object", "properties": { "color": { "type": "string", "required": true, /* comment la donnée sera affichée/éditée * le rôle 'color' : * - affichera un carré de couleur lorsqu'on l'affichera * - affichera un colorpicker lorsqu'on l'éditera */ "role": "color", // les champs suivants servent pour le formulaire "title": "Couleur de la donnée", "description": "Tooltip d'aide" } }, // les champs suivants aident à générer le formulaire d'édition "categories": [ { "title": "General", "keys": ["color"] } ] } ``` L'``id`` d'un schéma est construit de la manière suivante : * ``mydata`` * ``mydata.mydata2`` : ici ``mydata2`` hérite de ``mydata`` On obtient donc en base de données : ```javascript { "_id": "", "schema": // le schéma à proprement parler } ``` ## Étape 4 : gérer et servir la donnée Maintenant que nous avons la structure pour récupérer la donnée, et que nous sommes en mesure de la schématiser, il faut mettre en place les mécanismes permettant d'interagir avec et de la servir à l'UI. ### A. Les managers La gestion de la donnée est prise en charge par ce que l'on appelle les **managers**. Pour bien comprendre son rôle, il faut plonger un peu dans le code. Un **configurable** est un objet Python dont les propriétés sont définies par son fichier de configuration : ```python from canopsis.configuration.configurable import Configurable from canopsis.configuration.configurable.decorator import conf_paths from canopsis.configuration.configurable.decorator import add_category from canopsis.configuration.model import Parameter CONF_PATH = 'myfeature/manager.conf' # {sys.prefix}/etc/{CONF_PATH} CATEGORY = 'MYFEATURE' # Définition du contenu de la catégorie CONTENT = [ Parameter('foo'), Parameter('bar', parser=int) ] # ajoute un fichier de configuration à lire par le configurable @conf_paths(CONF_PATH) # permet de spécifier la catégorie de configuration depuis laquelle on peut lire les paramètres @add_category(CATEGORY, content=CONTENT) class MyFeatureManager(Configurable): pass ``` Ainsi, avec le fichier de configuration suivant : ```ini [MYFEATURE] foo=bar bar=42 ``` Ou : ```javascript { "MYFEATURE": { "foo": "bar", "bar": 42 } } ``` En effet, le configurable va tester différents drivers, actuellement on dispose de INI et JSON mais il est envisageable d'avoir un driver MongoDB ou autre Lorsque l'on instanciera la classe, on obtiendra : ```python obj = MyFeatureManager() assert obj.foo == "bar" assert obj.bar == 42 ``` Et à partir de cette classe ``Configurable`` on va définir l'arbre d'héritage suivant : ![Diagramme Configurable](https://lut.im/hAGCKkPTLn/IjNCgUAvCiHor3il) Un ``ConfigurableRegistry`` permet de spécifier, dans un paramètre de configuration, un autre ``Configurable`` à instancier : ```ini otherconfigurable_value = canopsis.myotherfeature.manager.MyOtherFeatureManager ``` Et on y accèdera, dans l'instance, de la manière suivante : ```python assert isinstance(self['otherconfigurable'], MyOtherFeatureManager) ``` Le ``MiddlewareRegistry`` fait de même pour les ``Middleware`` (qui identifient un **protocole** ainsi qu'un **type de données**) : ```ini mymiddleware_uri = protocol-datatype-datascope:// ``` De même que pour le ``ConfigurableRegistry``, on y accède de la manière suivante : ```python assert self['mymiddleware'].connected() ``` En général, un *manager* sera un ``MiddlewareRegistry``, ce qui permettra de changer de techno utilisée, sans modifier le code : ```ini [MYFEATURE] mystorage_uri = mongodb-timed-mydata:// # mystorage_uri = influxdb-timed-mydata:// mymanager_value = canopsis.myotherfeature.manager.MyOtherFeatureManager # mymanager_value = canopsis.myotherfeature.manager2.MyOtherFeatureManager2 ``` Et ce manager sera utilisé par le **moteur** et le **webservice**. En reprenant l'exemple du moteur : ```python from canopsis.common.utils import singleton_per_scope from canopsis.myfeature.manager import MyFeatureManager def event_processing(engine, event, manager=None, **_): if manager is None: # instancie la classe une seule fois par module manager = singleton_per_scope(MyFeatureManager) # faire des choses avec l'événement et le manager return event def beat_processing(engine, event, manager=None, **_): if manager is None: manager = singleton_per_scope(MyFeatureManager) # faire des choses avec le manager ``` ### B. Les webservices Afin de servir la donnée à l'UI, on dispose d'une application WSGI découpée en modules, que l'on appelle **webservice**. Ces derniers se trouvent dans le paquet Python ``canopsis.webcore.services``. Et grâce au code suivant (à placer dans le ``__init__.py``), on peut avoir plusieurs paquets Python fournissant du code à cet emplacement : ```python from pkgutil import extend_path __path__ = extend_path(__path__, __name__) ``` Bref, un *webservice* s'écrit très simplement : ```python # couche d'abstraction qui nous permettra de passer de Bottle à Flask from canopsis.common.ws import route from canopsis.common.utils import singleton_per_scope from canopsis.myfeature.manager import MyFeatureManager # Sera appelé par l'application WSGI pour charger les routes HTTP def exports(ws): # ici `ws` désigne donc le webserver manager = singleton_per_scope(MyFeatureManager) # ici on créé la route /foo qui accepte la méthode GET @route(ws.application.get) def foo(): return manager.foo # l'API retournera : # { # "total": 1, # "data": ["bar"], # "success": true # } # cette fois ci, on créé la route /bar/:baz @route(ws.application.get) def bar(baz): return (manager.bar == baz) ``` ## Étape 5 : l'application web Côté *backend*, on dispose désormais du nécessaire pour fournir à l'UI tout ce dont elle a besoin. Ainsi, nous avons également travaillé la modularité de cette UI, basée sur [Ember](http://emberjs.com), au maximum. ### A. Les briques Afin de répondre à cette problématique de modularité, nous avons mis en place un système de briques, permettant de sélectionner les fonctionnalités effectivement chargées. Concrètement, on peut voir une **brique** comme étant un *addon* apportant plusieurs fonctionnalités telles que : - nouveaux éléments graphiques (*composants*) - nouveaux outils de rendus (*widgets*, *renderers*) - nouveaux outils d'éditions (*éditors*) - ... Pour construire une brique, il suffit de créer un paquet [NPM](https://www.npmjs.com/) avec le ``package.json`` suivant : ```javascript { "name": "", "description": "", "version": "0.1.0", // il s'agit du fichier principal de la brique, il pointera vers la version de dev ou minifiée "main": "init.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "compile": "rm -Rf tmp/build && broccoli build tmp/build && cp tmp/build . -RT", "lint": "eslint src", "doc": "./node_modules/canopsis-ui-toolbelt/scripts/buildDoc", "minify": "node node_modules/canopsis-ui-toolbelt/scripts/minify.js" }, "repository": { "type": "git", "url": "" }, "author": "", "license": "AGPL-3.0", "devDependencies": { "broccoli": "^0.16.9", "broccoli-funnel": "^1.0.1", "broccoli-merge-trees": "^1.0.0", "broccoli-sourcemap-concat": "^1.1.6", "broccoli-template": "^0.1.1", "broccoli-uglify-js": "^0.1.3", // outil de gestion des briques Canopsis "canopsis-ui-toolbelt": "https://git.canopsis.net/canopsis/canopsis-ui-toolbelt/repository/archive.tar.gz?ref=", "jsdoc": "^3.3.0", "pre-commit": "^1.1.1" }, "pre-commit": [ "lint", "doc" ] } ``` Puis, dans un dossier ``src`` on placera le code source de la brique : - ``components/mycomponent/component.js`` - ``components/mycomponent/template.hbs`` - ``renderers/renderer-myrole.hbs`` - ``editors/editor-myrole.hbs`` - ``widgets/mywidget/controller.js`` - ``widgets/mywidget/mywidget.hbs`` - ``mixins/mymixin.js`` L'outil ``canopsis-ui-toolbelt`` se charge de : - récupérer récursivement tout le code JS dans le dossier ``src`` - référencer le code JS dans le fichier ``init.js`` qui représente la brique - récupérer récursivement tout les templates (``*.hbs``) dans le dossier ``src`` - référencer les templates également dans le fichier ``init.js`` - référencer les templates dans ``Ember.TEMPLATES`` : - pour un composant, il est requis d'avoir le dossier ``components/``, afin que le template soit reconnu comme étant un template de composant - pour le reste, le nom du template dans *Ember* sera le nom du fichier sans extension *NB: Le découpage n'est donc obligatoire que pour les composants, le reste peut être mis en vrac dans ``src``.* Une fois que l'on a créé tout cela (ou récupéré le dépôt Git), on peut finaliser la brique : ``` $ npm install $ npm run compile ``` Chaque fichier source contiendra au moins un **Ember Initializer** : ```javascript Ember.Application.initializer({ name: 'MyModule', after: ['Dependency1', 'Dependency2'], initialize: function(container, application) { var Dependency1 = container.lookupFactory('deptype:dependency1'), Dependency2 = container.lookupFactory('deptype:dependency2'); // do stuff application.register('modtype:mymodule', /* stuff */); } }); ``` Cela permet ainsi de s'assurer du bon chargement de chaque module. Au final, pour installer une brique, il suffit de : - cloner le dépôt finalisé dans : ``/opt/canopsis/var/www/canopsis`` - lancer la commande ``webmodulemanager enable mybrick`` afin de l'activer au chargement de l'UI L'ensemble des briques existantes (hormis celle par défaut livrées avec Canopsis) sont disponible [ici](https://git.canopsis.net/groups/canopsis-ui-bricks). ### B. Les composants Le composant, l'élément le plus basique de l'UI, sera utilisé par tout les autres éléments. Il s'agit simplement d'un composant *Ember* avec un template à fournir. On définira notre composant dans ``src/components/mycomponent/component.js`` : ```javascript Ember.Application.initializer({ name: 'MyComponent', after: [], initialize: function(container, application) { var MyComponent = Ember.Component.extend({ init: function() { this._super.apply(this, arguments); // faire des choses }, actions: { // on implémente les actions, déclenchable depuis le template foo: function() { alert('bar'); } } }); application.register('component:component-mycomponent', MyComponent); } }); ``` Et le template dans ``src/components/mycomponent/template.hbs`` : ```html

My awesome component

Launch action ``` ### C. Les renderers/éditeurs Une fois nos composants définis, on est en mesure de les utiliser dans des **renderers** ou des **éditeurs**. Le but de ces éléments est simple : - lorsqu'on demande l'affichage d'une donnée avec le composant ``renderer`` : - le champ ``role`` du schéma JSON est également lu - si un *renderer* du même nom est trouvé, il est utilisé dans le template - sinon, on affiche la donnée telle quelle - lorsqu'un formulaire d'édition est généré à partir du schéma JSON, en utilisant le composant ``editor`` : - le champ ``role`` est lu - si un éditeur du même nom est trouvé, il est utilisé dans le formulaire - sinon, on utilise l'éditeur par défaut Les *renderers*/*éditeurs* ne sont donc que de simple templates [HTMLBars](https://github.com/tildeio/htmlbars). On aura le *renderer* dans ``src/renderers/renderer-myrole.hbs`` : ```html

My rendered data: {{value}}

{{component-mycomponent}} ``` Et l'*éditeur* dans ``src/editors/editor-myrole.hbs`` : ```html

My data is being edited :

{{input type="password" value=attr.value}} {{component-mycomponent}} ``` ### D. Les widgets Enfin, on aura les **widgets**, qui seront en mesure d'utiliser *composants* et *renderers* afin d'implémenter des outils de visualisation de données plus complexes. Un *widget* est un **MVC** complet : - on implémente un contrôleur - on implémente des *mixins* qui viendront s'appliquer à la vue - on écrit un template Ce qui donne : ```javascript Ember.Application.initializer({ name: 'MyWidget', after: ['WidgetFactory'], initialize: function(container, application) { var WidgetFactory = container.lookupFactory('factory:widget'); var widgetOptions = {}; var MyWidgetViewMixin = Ember.Mixin.create({ didInsertElement: function() { this._super.apply(this, arguments); // faire des choses }, willDestroyElement: function() { this._super.apply(this, arguments); // faire des choses }, actions: { // actions de la vue } }); var widget = WidgetFactory('mywidget', { viewMixins: [ MyWidgetViewMixin ], actions: { // actions du contrôleur }, init: function() { // faire des choses }, findItems: function() { // méthode appelée lors de la récupération des données pour le template } }, widgetOptions); application.register('widget:mywidget'); } }); ``` Ensuite, il est nécessaire de créer un schéma ``widget.mywidget.json`` qui sera utilisé pour configurer le widget lors de son ajout dans une vue : ```javascript { "title": "MyWidget", "description": "Schéma de configuration de MyWidget", "metadata": { // icône dans le formulaire d'ajout de widget "icon": "fa fa-cog" }, "categories": [ { "title": "General", "keys": ["foo"] } ], "type": "object", "properties": { "foo": { "title": "Foo", // nom du champ dans le formulaire "description": "Foo field", // tooltip dans le formulaire "type": "string", "role": "myrole" } } } ``` ### E. Les mixins Lorsque l'on a des fonctionnalités communes à plusieurs widgets (comme le ``PeriodicRefresh`` qui actualise le widget régulièrement), il convient de factoriser le code en un **mixin**. Ce dernier pourra être ajouté et configuré au *widget* via l'UI, et ensuite appliquer la-dite configuration au contrôleur du widget : ```javascript Ember.Application.initializer({ name:'MyMixin', after: ['MixinFactory'], initialize: function(container, application) { var Mixin = container.lookupFactory('factory:mixin'); var get = Ember.get; var MyMixin = Mixin('mymixin', { mixinsOptionsReady: function() { this._super.apply(this, arguments); var mixinOptions = get('mixinOptions.mymixin'); // faire des choses } }); application.register('mixin:mymixin', MyMixin); } }); ``` Et il ne manque plus que le schéma ``mixin.mymixin.json`` décrivant la configuration du *mixin* : ```javascript { "title": "MyMixin", "description": "Schéma de configuration de MyMixin", "metadata": { // description affiché dans l'UI "description": "Add stuff to widget" }, "categories": [ { "title": "General", "keys": ["bar"] } ], "type": "object", "properties": { "bar": { "title": "Bar", "description": "Bar field", "type": "string" } } } ``` ### F. Les vues Tout les outils sont désormais à notre disposition pour construire nos *dashboards*. Donc si on résume : - une **vue** est composée de *widgets* - un *widget* est composé de *composants* et de *mixins* L'UI fournit les outils qui permettent de construire ces vues, le résultat final est un document JSON stocké en base : ```javascript { "_id": "id de ma vue", "description": "description de ma vue" "crecord_name": "nom de ma vue", "crecord_type": "view", "author": "", "enable": true, "internal": false, "tags": [], // le widget initial de la vue "containerwidget": { // identifiant du widget "xtype": "widgetcontainer", "title": "container title vbox", // le widget "widgetcontainer" s'attend à avoir un champ "items" "items": [ { // le widget "widgetwrapper" sert à inclure des widgets dans un container pour les placer correctement "xtype": "widgetwrapper", "title": "wrapper", // le widget encapsulé : "widget": { "xtype": "mywidget", "title": "My awesome widget", // configuration spécifique au widget "foo": "bar", // mixins appliqués via l'UI : "mixins": [ { "name": "periodicrefresh", // paramètres du mixin "refreshInterval": 60 }, { "name": "mymixin", // paramètres du mixin "bar": "baz" } ] } } ], // le container de base de la vue à un widget de layout par défaut "mixins": [ { "name": "lightlayout" } ] } } ``` La totalité de l'UI est générée à partir de ces vues JSON, et est donc complètement personnalisable. # Le bac à événements Parmi les vues par défaut qui sont livrées avec Canopsis, on trouve le **Bac à événements**. Cette vue fournit un *dashboard* de supervision commun, unifiant ainsi la totalité des superviseurs remontant des informations à Canopsis. ![screenshot du bac à événements](https://lut.im/NO9CRRuDvI/2WWIKgCUrk6EtY1u) Depuis ce bac, on voit apparaître les différentes alarmes avec comme informations : - leurs sources d'émission (le connecteur) - leurs sources cible (composant/ressource, qui dans la plupart des cas correspond au couple *host*/*service*) - le dernier message associé à un *check* (Nagios, Shinken, Centreon, ...) - le statut de l'alarme associé au *check* : - **Off** : aucune alarme n'est présente - **On Going** : un problème a été remonté et n'est toujours pas résolu - **Stealthy** : une alarme a été remontée et est immédiatement repassée OK (durée paramétrable) - **Flapping** : il y a eu X changements d'état en 1h sur l'alarme (durée et fréquence paramétrable) - **Cancelled** : l'alarme a été annulée par un utilisateur (pour éviter les faux-positifs) - l'état du *check* : - **INFO** : tout va bien - **MINOR** : équivalent au *Warning* de Nagios - **MAJOR** : équivalent au *Critical* de Nagios - **CRITICAL** : les *Unknown* de Nagios sont remontés en tant que tel, mais cela ne se limite pas à cette notion - **UNKNOWN** : état non pris en charge à l'heure actuelle, c'est tout ce qui est supérieur à *CRITICAL* (``3``) - la présence d'un acquittement et/ou d'un ticket : - les connecteurs pour Nagios, Shinken, etc... peuvent remonter les acquittements posés - depuis Canopsis, on peut en poser manuellement - la date du dernier changement d'état Sur chaque alarme, on peut réaliser différentes actions : - l'acquittement (permet de déverrouiller les autres actions), cela émettra le même événement qui serait remonté par un superviseur, soit un événement de type ``ack`` - une suppression de l'acquittement, cela émettra un événement de type ``ackremove`` - une annulation de l'alarme, cela émettra un événement de type ``cancel`` - une fois l'alarme annulée, on peut annuler cette action, cela émettra un événement de type ``uncancel`` - une déclaration de ticket : - cela émettra un événement de type ``declareticket`` - cet événement pourra être capturé par le moteur ``event_filter`` pour déclencher un job (voir plus bas) qui communiquera le ticket à un outil tiers (par exemple [iTop](http://www.combodo.com/itop)) - une association de ticket existant : - cela émettra un événement de type ``assocticket`` - on peut imaginer que le job qui communique la déclaration du ticket à l'outil tiers récupère le numéro du ticket nouvellement créé, et l'associe automatiquement - une requalification de l'événement : - cela changera l'état du *check* manuellement, et ce dernier gardera cet état jusqu'à la résolution de l'alarme - cela émettra un événement de type ``check``, comme le superviseur - la seule différence est la présence d'un champ ``keep_state`` qui vaut ``true`` dans l'événement Toutes ces actions permettent ainsi d'unifier une supervision hétérogène, et l'administrateur (technique) ne devra utiliser/maîtriser qu'un seul outil. # Sélecteur et widget météo : simplifier la supervision Lorsque l'on supervise une grosse infrastructure, les informations remontées via les *checks* deviennent tout de suite beaucoup plus conséquentes. C'est pourquoi nous avons mis en place la possibilité d'agréger ces *checks* afin d'avoir une visibilité plus simple sur l'infra. Cette agrégation se fait à l'aide des **sélecteurs** : - on créé un filtre d'événements - on applique un algorithme à l'état de chaque *check* qui matche le filtre (actuellement seul l'algo **Worst State** est disponible) - on produit un événement de type ``selector`` qui contient : - l'état agrégé - le champ ``output`` qui est le rendu du template spécifié dans la configuration du sélecteur - si le sélecteur est configuré pour, on déclenche le calcul des **SLA** : - sur une période de temps (spécifiée dans la configuration du sélecteur) - on calcule le pourcentage de temps passé sur chaque état possible - on produit une *métrique* pour chacune de ces métriques, ainsi qu'un événement de type ``sla`` - l'état remonté par l'événement correspond aux seuils de SLA configurés dans le sélecteur Le résultat est finalement affichable avec un widget **weather** : ![screenshot widget weather](https://lut.im/rFj8rArPDg/88fPzFC0Y15K5B00) *NB: Le sélecteur peut afficher également des checks unitairement* On peut ainsi noter les couleurs suivantes : - *vert* : l'état du sélecteur est ``INFO`` - *jaune* : l'état du sélecteur est ``MINOR`` - *orange* : l'état du sélecteur est ``MAJOR`` - *rouge* : l'état du sélecteur est ``CRITICAL`` - *violet* : toutes les alarmes du sélecteur ont été acquittée - la couleur du widget est celle du pire état des sélecteurs positionnés dans celui ci Un clic sur le sélecteur dans le widget nous redirigera sur le *Bac à événements*, filtré avec le filtre du sélecteur. # Monitoring et séries Chaque connecteur, moteur, et sélecteur produisent des données de performances : - temps d'exécution d'un *check* - usage CPU/RAM/Disque - temps moyen passé sur un événement - nombre moyen d'événements par seconde - donnée de SLA - ... Tout cela est remonté dans Canopsis dans un événement via le champ ``perf_data_array`` : ```javascript { // info de l'événement classique "perf_data_array": [ { "metric": "nom_de_ma_metrique", "value": 42.1337, "type": "GAUGE", // GAUGE, COUNTER, ABSOLUTE ou DERIVE // champs optionnels "unit": "...", "min": 0, "max": 1337.42, "warn": 1000, "crit": 1300 } ] } ``` Ces données vont être historisée dans Canopsis. On peut donc noter 4 types de métriques : - ``GAUGE`` : on historise la valeur telle quelle - ``COUNTER`` : lorsque l'on récupère la valeur, on fait l'addition des valeurs historisées - ``ABSOLUTE`` : on historise la valeur absolue - ``DERIVE`` : il s'agit de la valeur dérivée par rapport au temps Une métrique est ensuite identifiée par : - le composant de l'événement - la ressource de l'événement - le nom de la métrique dans le tableau de *perfdata* Le tout peut être affiché dans un chart : ![screenshot de timechart](https://lut.im/t5UOefHQpa/8ZHYSdBxxbODOJSw) On est ainsi en mesure de sélectionner un ensemble de métrique avec un filtre basé sur des expressions régulières : - ``co:.*\.myhost re:cpu-.* me:system me:user me:wait`` Qui se traduit en filtre MongoDB : ```javascript { '$and': [ {'component': {'$regex': '.*\.myhost'}}, {'resource': {'$regex': 'cpu.*'}}, { '$or': [ {'name': {'$regex': 'system'}}, {'name': {'$regex': 'user'}}, {'name': {'$regex': 'wait'}} ] } ] } ``` Une fois les identifiants de métriques récupérés, on peut aller demander les points stockés en base, dans une fenêtre de temps bien définie. Une **série** est donc munie : - d'un filtre de métrique - d'une période d'agrégation avec un opérateur d'agrégation (le manager de *perfdata* nous retournera les données agrégées) - d'une période de consolidation - d'une formule de consolidation Ici la partie consolidation sert à consolider les différents points agrégés en un seul, afin de produire une nouvelle métrique. La formule se construit de la manière suivante : - on a des opérateurs qui prennent en paramètre un filtre de métrique qui sera appliqué sur l'ensemble de métriques déjà sélectionnées - ces opérateurs retournent un point consolidé - on peut les utiliser dans une expression mathématique classique Par exemple, ``SUM("me:.*") / COUNT("me:.*")``, permet de réaliser une moyenne. # Les tâches ordonnancées et les notifications Parmi les moteurs de Canopsis, certains sont dédiés à une fonction précise : exécuter une tâche. Il y a donc un moteur **scheduler** qui, régulièrement, va chercher à exécuter des jobs configurés selon une [règle de récurrence](http://www.kanzaki.com/docs/ical/rrule.html). En fonction du type de job, ce dernier sera redirigé au moteur correspondant, que l'on appellera un **taskhandler**. Cela permet de construire un équivalent de ``crontab`` au sein de Canopsis. Ces *taskhandlers* ne servent pas uniquement à l'exécution de tâches ordonnancées, ils peuvent être utilisés en tant que notification : - une règle du moteur *event_filter* peut déclencher l'exécution d'un job si l'événement reçu matche le filtre de la règle - par exemple, à la réception d'un événement ``declareticket``, on peut lancer l'exécution d'un job réalisant une requête d'insertion de ticket à un outil tiers # Conclusion Grâce à tout ces éléments, Canopsis est en mesure de répondre à de nombreux besoins, allant de la supervision simple, à l'analyse poussée de données afin de générer des rapports sur une infrastructure (ou autre). Notre objectif premier est la modularité du projet, afin de pouvoir fournir une solution sur mesure et de ne pas transformer l'outil en énorme usine à gaz. Pour résumer, on a répondu à cette problématique avec : - le découpage du *backend* en projets Python embarquant : un *manager*, éventuellement un moteur et un *webservice* - le découpage du *frontend* en briques embarquant : des composants, des *renderers*, des *éditeurs*, des *mixins*, des *widgets* - la schématisation des données et à l'avenir des actions possibles sur cette dernière (transformation, schématisation de l'API des *managers*, ...) - le développement d'API générique permettant le changement de technologies sans modification du code Beaucoup de choses ont été faites, et beaucoup de travail reste à faire, notamment : - la finalisation des rôles **Ansible** - l'intégration d'une notion de graphe pour les entités qui sont le centre de toutes les données stockées par Canopsis, afin de rendre le système complètement réflexif - une séparation totale du *backend* et du *frontend*, permettant d'utiliser l'un sans l'autre - génération de code à partir des schémas - ... Bref, Canopsis est en constante évolution, et touche à de nombreuses problématiques toutes plus intéressantes les unes que les autres.

AltStyle によって変換されたページ (->オリジナル) /