Il y a 2 mois · 14 minutes · Back, DevOps

Superviser mon application Play! avec Prometheus

logo

Il nous semble clair que l’industrie de l’informatique s’est finalement mis d’accord sur trois choses :

  1. Nous avons besoin de faire du code de qualité
  2. Il faut intégrer le code de manière continue
  3. Il faut superviser ce que nous installons

Les pratiques du craftsmanship, en particulier le refactoring, couplées à un ensemble d’outils de test permettent d’affronter le premier. La construction de pipelines d’intégration avec des usines comme Jenkins, GoCD ou TravisCI permettent d’attaquer le deuxième. Cependant, la supervision, la situation n’est pas aussi claire. Du coup, il existe une grande variété de fonctionnalités (stack) pour superviser la JVM, suivre des métriques métiers, gérer des alertes, etc. La popularisation des architectures distribuées (dont les microservices) n’a fait qu’empirer la problématique.

Pour résoudre tous ces problèmes, nous avons choisi une stack composée de Prometheus, CAdvisor et Grafana et nous allons vous montrer comment les utiliser pour superviser une application de site web basée sur Play! framework.

Le choix de la stack de supervision

Finalement, nous avons terminé notre application Play!. Nous avons fait nos tests et notre chaîne d’intégration continue et l’application est même installée dans l’environnement de UAT (User Acceptance Testing). C’est à ce moment-là que nous réalisons que nous n’avons pas pensé à la supervision !

Après une rapide recherche, nous trouvons qu’il existe en fait deux modèles de communication : le push et le pull. Dans le modèle push, l’application monitorée envoie régulièrement ses métriques à l’outil de monitoring. Typiquement, il existe entre l’application et l’outil de monitoring un agent intermédiaire qui est chargé de la collection des métriques. Dans le modèle pull, c’est l’outil de monitoring qui connaît les applications monitorées et les requête à une fréquence spécifique.

En approfondissant notre recherche nous trouvons plusieurs alternatives :

positionnement

Les alternatives

1. Telegraf + InfluxDB

C’est une stack basée sur une architecture de push.

Telegraf est un agent qui récupère et exporte des métriques vers un système de stockage comme InfluxDB. InfluxDB est une base de données de type time series utilisée pour le stockage de métriques.

Ces deux outils sont en général utilisés avec Kapacitor. Il fait le pilotage des données et la gestion des alertes. Pour exemple, Kapacitor permet d’évaluer une métrique dans une fenêtre de 5 ms, la compare avec un seuil puis envoie une alerte sur Slack.

InfluxDB

2. Statsd + Graphite

Également, cette stack est basée sur une architecture de push.

Statsd est un agent réseau (UDP et TCP) qui permet d’écouter des statistiques et de les envoyer vers un autre système comme Graphite.

Graphite est capable de stocker des données de type time series (c’est-à-dire qu’elles sont ordonnées dans le temps) et fournit également un langage de requêtage. Il présente une approche similaire à celui de Telegraf et InfluxDB. L’ajout des informations complémentaires est basée sur la création des nouvelles métriques. Les deux métriques suivantes permettent de compter le nombre de requêtes en succès ou en erreur pour le path /login

stats.api-server.login.get.200 -> 93
stats.api-server.login.get.500 -> 45

Un exemple de l’intégration de graphite se montre comme suit

graphite-grafana

3. Nagios

Cette stack est basée sur un modèle de pull.

Nagios est un outil spécialisé surtout sur le monitoring de l’infrastructure. Il est donc plutôt utilisé comme une alternative à sensu. Il comporte un nombre important de plugins mais il ne fait pas le stockage des données, nous devons donc recourir à d’autres outils pour gérer la persistance.

Le monitoring d’une application basée sur la JVM (comme Play!) se fait typiquement en utilisant JMX ou en s’intégrant avec statsd.

perfdata

4. Prometheus

Encore une autre stack qu’utilise une approche de pull.

Prometheus est un système de monitoring et d’alerting. L’approche pull nous intéresse énormément parce que nous pouvons maîtriser les données métier que nous voulons rendre publiques dans les applications de monitoring. En fait pour Prometheus, la seule contrainte imposée à l’application monitorée est d’être capable de produire du texte avec HTTP. Avec cet outil, il est possible d’ajouter des ensembles de clés-valeurs pour rendre le monitoring plus dynamique. La métrique suivante est donc parfaitement valide :

http_request_duration_seconds_bucket{le="0.02",method="GET",path="/assets/:file",status="2xx"} 23

Une vision à plus haut niveau sur l’intégration d’une application avec Prometheus est montrée dans l’image suivante :

prometheus-grafana

Cet outil va rester notre choix pour le reste de cet article.

La visualisation

Cependant, il nous reste à choisir l’outil pour la visualisation de nos métriques. Les alternatives les plus utilisées sont :

  • Kibana + Timelion. Kibana est l’outil de visualisation par défaut dans Elasticsearch. Timelion permet l’intégration de timeseries dans Kibana. Ensemble, ces outils aident à la visualisation mais surtout à l’analyse et l’exploration des données au travers de multiples dimensions.
  • Grafana. Cet outil a été conçu pour faciliter la visualisation de dashboards complexes basées sur des sources timeseries.

Dans notre exemple, nous n’avons pas besoin d’explorer les données et nos dashboards ne sont pas destinés à un utilisateur final. Ce qui nous intéresse est de pouvoir afficher les métriques, vérifier l’état de santé de notre application et dans le futur, pouvoir créer des alarmes. Grafana sera par conséquent l’outil choisi pour la visualisation de nos métriques.

Les métriques exposées par l’application basée sur Play! Framework

Pour pouvoir exposer les métriques depuis notre application, il nous faut importer une librairie cliente. Nous avons choisi ce client non officiel pour Scala car il s’adapte un peu mieux à la philosophie compile time safe de Scala, mais nous aurions également pu utiliser le client officiel. Pour l’utiliser dans le projet il suffit de l’ajouter dans le fichier build.sbt

libraryDependencies += "org.lyranthe.prometheus" %% "client" % "0.9.0-M1"

L’exposition des métriques

Prometheus va régulièrement demander à l’application (toutes les 15 secondes par défaut) ses nouvelles métriques. Nous devons donc fournir une manière de créer les métriques que nous allons calculer. Nous appelons cette classe un MetricBuilder. Ce builder permet la création de tous les objets qui vont stocker la valeur courante pour chaque métrique (Counter, Gauge, Histogram et autres). Chaque métrique doit s’enregistrer auprès d’un Registry pour pouvoir être requêtée le moment venu avec un appel au outputText. L’implémentation de ces objets est fournie par le client de Prometheus, il suffit de les configurer et les instancier.
class PrometheusMetricBuilder @Inject()(lifecycle: ApplicationLifecycle, implicit val prometheusRegistry: Registry) { 
  def buildOutputText: String = prometheusRegistry.outputText // prints all metric values
  ... // other metrics
}

Nous devons exposer une url avec toutes les valeurs courantes pour chaque métrique. Nous allons le faire depuis un contrôleur en utilisant l’implémentation du Registry injectée dans notre Builder.

@Singleton
class PrometheusMetricsController @Inject()(metricBuilder: PrometheusMetricBuilder) extends Controller {
  def metrics = Action {
    val samples = new StringBuilder()
    val writer = new WriterAdapter(samples)
    writer.write(metricBuilder.buildOutputText)
    writer.close()

    Result(
      header = ResponseHeader(200, Map.empty),
      body = HttpEntity.Strict(ByteString(samples.toString), Some("text/plain"))
    )
  }
}

Nous pouvons exposer ce traitement dans le path que nous souhaitons. L’url /metrics semble être une bonne idée. Rajoutons-là à notre fichier routes.

GET     /metrics                    controllers.PrometheusMetricsController.metrics

Nous n’allons pas rentrer dans le détail des types de métriques, le but étant de rechercher une solution rapide pour les premières métriques de notre nouvelle application. Nous proposons alors les trois métriques suivantes :

Les métriques

1. Le nombre de visites dans l’application

Supposons que nous voulions monitorer le nombre absolu de visiteurs. Notre objectif est de pouvoir nous donner une idée du succès de notre site. Celui-ci est un nombre qui augmentera toujours. Pour créer cette métrique il suffit de construire une instance de type Counter et l’enregistrer auprès d’un Registry de métriques fourni par le client de Prometheus.

@Singleton
class PrometheusMetricBuilder @Inject()(lifecycle: ApplicationLifecycle, implicit val prometheusRegistry: Registry) {
  ...
  val counter = Counter(metric"play_requests_total", "Total requests.").labels().register
  ...
}

Ensuite, nous pouvons l’utiliser dans notre contrôleur.

@Singleton
class HomepageController @Inject()(builder: PrometheusMetricBuilder) extends Controller {
  def index = Action {
    builder.counter.inc()
    Ok(views.html.index("Your new application is ready."))
  }
}

2. Le nombre d’utilisateurs connectés

Le nombre d’utilisateurs connectés est une métrique qui doit pouvoir s’incrémenter et se décrémenter par rapport au nombre de connexions et déconnexions, voire des abandons de page. Prometheus appelle ce type de composant un Gauge. Nous devons également écouter le hook (l’envoi du message) de finalisation de l’application pour prendre en compte les utilisateurs qui ferment leur navigateur. Pour cela nous avons une instance de l’ApplicationLifecycle qui nous donne un callback lors de l’arrêt de l’application. Nous allons procéder de manière similaire à la métrique précédente.

@Singleton
class PrometheusMetricBuilder @Inject()(lifecycle: ApplicationLifecycle, implicit val prometheusRegistry: Registry) {
  ...
  val gauge = Gauge(metric"play_current_users", "Actual connected users").labels().register
  lifecycle.addStopHook { () => Future.successful(gauge.dec()) }
}

Par contre, la logique de ce contrôleur est plus complexe car nous devons augmenter la valeur lors du login, et la réduire lors du fermeture de la session.

@Singleton
class SessionController @Inject()(builder: PrometheusMetricBuilder) extends Controller {
  def login = Action {
    builder.gauge.inc()
    Ok("Logged in ...")
  }
  def logout = Action {
    builder.gauge.dec()
    Ok("Logged out!")
  }
}

3. Le temps de réponse par url

Le temps de réponse est typiquement analysé grâce aux échantillons pris pour chaque route disponible dans notre application. Dans la terminologie de Prometheus, ce type de métrique est appelé un histogramme.

Pour effectuer l’échantillonnage, nous devons créer un filtre qui met à jour un composant de type Histogram avec les informations suivantes:

  • La méthode HTTP
  • Le path
  • Le status de la réponse
  • Le temps de réponse

Il suffit d’ajouter le composant dans notre PrometheusMetricBuilder :

@Singleton
class PrometheusMetricBuilder @Inject()(lifecycle: ApplicationLifecycle, implicit val prometheusRegistry: Registry) {
  ...
  val httpRequestLatency =
    Histogram(metric"http_request_duration_seconds",
      "Duration of HTTP request in seconds")(httpHistogramBuckets)
      .labels(label"method", label"path", label"status")
      .register
  ...
}

Nous pouvons écrire ce filtre comme suit :

class PerformanceFilter @Inject()(implicit
                                  val mat: Materializer,
                                  builder: PrometheusMetricBuilder,
                                  executionContext: ExecutionContext
                                ) extends Filter {
  ...
  def apply(nextFilter: RequestHeader => Future[Result])(
    requestHeader: RequestHeader): Future[Result] = {
    val timer = Timer()
    val future = nextFilter(requestHeader)
    getRouteDetails(requestHeader) match {
      case Some(details) =>
        future.onComplete {
          time(timer) { statusCode =>
            builder.httpRequestLatency.labelValues(
              details.method,
              details.route,
              statusCode)
          }
        }

      case None =>
        // rien à faire ici
    }
    future
  }
}

Une fois notre application exécutée, testons le lien localhost:9000/metrics pour pouvoir vérifier les valeurs de nos deux métriques.

play_current_users 0.0
play_requests_total 1.0
http_request_duration_seconds_bucket{le="1.0E-4",method="GET",path="/",status="2xx"} 0
http_request_duration_seconds_bucket{le="0.005",method="GET",path="/metrics",status="2xx"} 0
http_request_duration_seconds_bucket{le="0.01",method="GET",path="/metrics",status="2xx"} 1

Nous avons un code capable d’exposer des métriques, mais il faut maintenant se connecter à Prometheus, CAdvisor et Grafana.

Tester en local avec docker-compose

Pour éviter de tout installer en local, nous fournissons un repository Github avec la configuration initiale basée sur docker-compose qui vous permettra de démarrer toute l’infrastructure que nous venons de décrire :

  • Un container pour Prometheus
  • Un container pour CAdvisor
  • Un container pour l’application avec le framework Play!
  • Un container pour Grafana

Pour tout démarrer, il suffit d’exécuter :

docker-compose up -d

Une fois terminé, vous devriez avoir quatre containers :

Containers

Ces differents containers sont liés de la manière suivante :

Docker-engine

CAdvisor

Le Container Advisor permet d’exposer la consommation des resources des container docker. Vu que nous allons utiliser Pometheus et d’autres composants dans docker, CAdvisor semble une très bonne idée car il est nativement capable d’exposer des métriques sur la consommation réseau, mémoire et disque vers Prometheus. Si nous allons à  http://localhost:8080/containers/ nous allons voir un résultat similaire à celui-ci :

Metrics

La configuration de Prometheus

La configuration de Prometheus est basée sur un fichier nommé prometheus.yml dans lequel nous devons spécifier les endpoints de ce que nous allons monitorer. Chacun peut avoir sa propre configuration contenant le target (combinaison hôte et port), le path url où l’application expose ses métriques, l’intervalle d’évaluation et d’autres options si nécessaire.

global:
  scrape_interval:     15s
  evaluation_interval: 15s
  external_labels:
    monitor: 'monitoring-play-app'
scrape_configs:
  - job_name: 'prometheus'
    scrape_interval: 5s
    static_configs:
      - targets: ['cadvisor:8080']
  - job_name: 'playframework-app'
    scrape_interval: 5s
    metrics_path: '/metrics'
    static_configs:
      - targets: ['play-app:9000']

Vu que nous lions les conteneurs entre eux, il est possible d’utiliser le nom de notre container en tant qu’adresse IP. L’exemple précédent permet de monitorer trois services :

  • CAdvisor. Qui tourne dans un container appelé cadvisor sur le port 8080.
  • notre application Play!. De manière similaire, notre application est disponible à l’url play-app:9000.

Pour vérifier que nos targets sont bien prises en compte il nous suffit de vérifier directement sur Prometheus à l’adresse http://localhost:9090/targets. Ici, nous utilisons localhost pour parler avec le container parce nous sommes sur linux. Vous obtenez le même résultat si vous utilisez Docker for mac ou Docker for windows.

Targets

Il est également possible de requêter nos métriques sur http://localhost:9090/graph. Dans cette page nous pouvons :

  • Vérifier que nos métriques sont bien prises en compte.
  • Vérifier que les métriques se modifient dès que nous requêtons notre application Play!
  • Tester des opérations de regroupement (sommes, moyennes, et autres)
 check
  • Prévisualiser nos métriques
 graph-current

Cependant, ceci n’est pas vraiment un outil de visualisation et pour cela nous avons besoin d’un logiciel spécialisé.

Grafana

La suite de visualisation de métriques Grafana va nous permettre de suivre en temps réel les métriques de notre application. L’interface est accessible depuis http://localhost:3000.

Nous allons créer une data source pour pouvoir interroger le service de Prometheus, afin de lier nos métriques à la visualisation de Grafana.

Source

Ensuite, nous devons ajouter un dashboard qui affichera les métriques exposées des targets de Prometheus. Heureusement, dans le repository Github nous fournissons un dashboard prêt à l’emploi. Il permet la visualisation des métriques envoyées par notre application et ceux de CAdvisor. Il suffit alors d’importer le fichier Grafana_Dashboard.json. Une fois installé, nous avons un panel qui montre le résultat de nos métriques de visites et d’utilisateurs connectés.

data

Nous devons avoir également un histogramme avec le temps de réponse des url de notre application :

response time

De plus, grâce au CAdvisor nos avons des métriques de consommation de mémoire, processeur et disque de tous nos containers :

CPU

Et voilà, nous avons finalement un outil de monitoring intégré à notre application.

Conclusion

Nous avons abordé les différences entre les modèles push et pull pour la communication avec les outils de monitoring, puis nous avons comparé quelques outils et finalement nous avons choisi Prometheus.

Ensuite, nous avons montré comment exposer des métriques sur un endpoint depuis une application basée sur Play! framework. Finalement nous avons montré le résultat final avec un docker-compose contenant entre autre un dashboard Grafana pour le suivi en temps réel des métriques.

Nous pouvons conclure qu’au final, les outils qui permettent d’avoir des métriques sont maintenant assez simples d’utilisation.

Quelques problèmatiques restent toujours ouvertes comme par exemple la manière d’intégrer de multiples instances de notre application ou l’intégration d’un système d’alertes. Nous aborderons ces sujets lors d’un prochain article.

Aurore De Amaral
Développeuse scala ~ java intéressée par la data, le craftmanship et chez Xebia depuis octobre 2016..
Fabian Gutierrez
Arrivé à Xebia en 2014, il est passionné par le développement logiciel surtout dans la JVM

2 réflexions au sujet de « Superviser mon application Play! avec Prometheus »

  1. Publié par Fabien Baligand, Il y a 1 mois

    Un point m’échappe : dans promotheus, vous définissez uniquement l’URL qu’il faut « pull » toutes les n secondes, mais vous ne définissez pas comment parser le contenu de la réponse http.
    Comment s’opère cette magie ?

  2. Publié par Fabian, Il y a 4 semaines

    Pas besoin de l’indiquer comment parser le contenu parce que le client qu’on utilise imprime le valeurs de métriques (prometheusRegistry.outputText) avec le format attendu par prometheus

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *