Présentation technique de Canopsis

16
7
fév.
2016
Supervision

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, elle se veut simple d'utilisation, et suffisamment souple pour répondre à un maximum de besoin.

Sommaire

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)
  • 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

É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.

Le cas le plus simple est donc celui ci :

#!/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": "<hostname>",
    "resource": "<job's name>",
    # données portées par l'événement
    "state": 0,  # 0 = INFO, 1 = MINOR, 2 = MAJOR, 3 = CRITICAL
    "output": "<message>"
}

# 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.

Schema RabbitMQ

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 :

[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 à :

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 :

{
    "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 :

{
    "_id": "<id du schema>",
    "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 :

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 :

[MYFEATURE]

foo=bar
bar=42

Ou :

{
    "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 :

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

Un ConfigurableRegistry permet de spécifier, dans un paramètre de configuration, un autre Configurable à instancier :

otherconfigurable_value = canopsis.myotherfeature.manager.MyOtherFeatureManager

Et on y accèdera, dans l'instance, de la manière suivante :

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) :

mymiddleware_uri = protocol-datatype-datascope://

De même que pour le ConfigurableRegistry, on y accède de la manière suivante :

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 :

[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 :

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 :

from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)

Bref, un webservice s'écrit très simplement :

# 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, 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 avec le package.json suivant :

{
    "name": "<nom de la brique>",
    "description": "<description de la brique>",
    "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": "<url du dépôt>"
    },
    "author": "<auteur>",
    "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=<branche git de canopsis-ui-toolbelt>",
        "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/<mycomponent>, 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 :

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.

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 :

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 :

<h1>My awesome component</h1>

<a {{action foo}}>Launch action</a>

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.

On aura le renderer dans src/renderers/renderer-myrole.hbs :

<p>My rendered data: {{value}}</p>

{{component-mycomponent}}

Et l'éditeur dans src/editors/editor-myrole.hbs :

<p>My data is being edited :</p>
{{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 :

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 :

{
    "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 :

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 :

{
    "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 :

{
    "_id": "id de ma vue",
    "description": "description de ma vue"
    "crecord_name": "nom de ma vue",
    "crecord_type": "view",
    "author": "<user qui a créé la vue>",
    "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

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)
  • 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

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 :

{
    // 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

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 :

{
    '$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.
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.

Aller plus loin

  • # jouer sur les mots

    Posté par  . Évalué à 0.

    solution d'hypervision (on évite l'appellation hyperviseur qui reste dans le langage courant spécifique à la virtualisation)

    Je suis le seul à voir les deux problèmes de cette phrase ?

    1) La subtile différence entre -ion et -eur qui d'une part est très confusante, et d'autre part est juste fausse vu le nombre de personne qui parlent d'hypervision pour la virtualisation.

    2) Le super gros lol sur "langage courant": j'en ai parlé dans la rue et personne n'a compris.

    Bon et pour rigoler: hyper en grec ça veut dire "trop", ça veut pas dire "encore plus gros que ce que j'ai appelé super hier". Mais ça c'est pas ta faute, le jargon est ce qu'il est on est tous obligé de suivre. D'où d'ailleurs le problème du 1.

    • [^] # Re: jouer sur les mots

      Posté par  (site web personnel) . Évalué à 5.

      Quand je dis "langage courant" ça reste bien sûr dans le domaine de l'informatique. Tu le dis toi même : "vu le nombre de personne qui parlent d'hypervision pour la virtualisation".

      Donc bien sûr que c'est un jeu de mot, avec "hypervision" on accepte beaucoup plus facilement l'idée que cela se place au dessus (étymologie de hyper) de la supervision.

      https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg

  • # Ouf

    Posté par  . Évalué à 3.

    Merci de cette dépêche !

    Ça a l'air vraiment très sophistiqué…. Probablement trop pour moi.

    J'ai un peu du mal à comprendre la différence que tu fais entre le monitoring et l'hypervision. Les 2 servent à vérifier que ton système fonctionne.

    Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

    • [^] # Re: Ouf

      Posté par  (site web personnel) . Évalué à 2.

      Merci pour le retour :)

      Personnellement, je pense que la différence est la même qu'entre supervision et monitoring, c'est à dire qu'on introduit une méthodologie pour la résolution d'éventuels problèmes (identification, acquittement, déclaration de ticket, résolution).

      Ici l'hypervision complète cette méthodologie, en unifiant les différentes solutions de supervision, et en remontant des statistiques sur cette méthodologie (SLA sur les durées d'acquittement, de résolution des problèmes et de disponibilité des services par exemple).

      Le monitoring permet de surveiller (vérifier que le système fonctionne), et la supervision/hypervision permettent de prendre une décision quand il ne fonctionne pas (quel est le problème et la procédure à appliquer pour le résoudre ?).

      Je pense aussi qu'on ne peut avoir l'un sans l'autre pour être réellement efficace.

      https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg

      • [^] # Re: Ouf

        Posté par  . Évalué à 1.

        Monitoring et Supervision, c’est a priori la même chose, mais ça ne fait pas d’Hypervision.

        Monitoring et Supervision : collecte d’informations et décisions sommaires comme envoi de mails… pour rester dans les vieux trucs. Peut embarquer de la métrologie, mais ne s’en contente pas. Sinon c’est qu’on a un Munin ou un Cacti, qui ne font pas de supervision/monitoring. Nagios fait de la supervision ou du monitoring, et l’aspect métrologie n’existe que parce qu’on peut interfacer ce qu’il remonte à d’autres outils.

        Hypervision : ajoute de l’intelligence à la sup / le monitoring, ce que fait canopsis quoi.

        • [^] # Re: Ouf

          Posté par  (site web personnel) . Évalué à 1.

          J'ai un peu l'impression que Shinken ajoute cette même intelligence, alors que c'est vendu comme une solution de supervision.

          • [^] # Re: Ouf

            Posté par  . Évalué à 1.

            Shinken, il se contente de planter.

            • [^] # Re: Ouf

              Posté par  . Évalué à 2.

              Chez-moi-ça-marche (tm)

              • [^] # Re: Ouf

                Posté par  . Évalué à 1.

                Tu veux dire, quand tu lui demandes de faire le strict minimum ? C’est à dire pas de HA et tout ? Dans ce cas là, oui, ça marche.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.