À la découverte de RUM

Dire qu’à une lettre près, vous pourriez être en train de lire la recette d’un bon cocktail à base de rhum... Cet article de blog prendrait alors une orientation (et une saveur !) totalement différente. Mais non ! Aujourd’hui, nous allons nous intéresser à la fonction RUM d’Elastic. Et croyez-moi, elle est tout aussi délicieuse ! Voici maintenant l’heure d’y goûter. Avant de nous lancer, je voudrais vous prévenir que les informations que je vais aborder aujourd’hui sont assez vastes, et que cela prendra donc du temps.

RUM, qu’est-ce que c’est ?

La fonction Real User Monitoring, ou RUM, d’Elastic capture les interactions des utilisateurs avec un navigateur web et fournit une vue détaillée de l’"expérience utilisateur réelle" de vos applications web sur le plan des performances. L’agent RUM d’Elastic est un agent JavaScript, ce qui signifie qu’il prend en charge n’importe quelle application basée sur JavaScript. RUM peut fournir des informations précieuses sur vos applications. Voici quelques avantages de RUM parmi les plus courants :

  • Les données que RUM fournit sur les performances peuvent vous aider à identifier les goulets d’étranglement, ainsi que l’incidence des problèmes de performances des sites sur l’expérience de vos visiteurs.
  • Les informations des agents utilisateur capturées par RUM vous permettent d’identifier les navigateurs, les appareils et les plateformes les plus utilisées par vos clients. Ainsi, vous pouvez apporter des améliorations optimales à votre application.
  • Combinées aux informations sur l’emplacement, les données sur les performances des utilisateurs individuels de RUM vous aident à comprendre les performances régionales de votre site web au niveau mondial.
  • RUM fournit des informations et des mesures pour les accords de niveau de service (SLA) de votre application.
  • RUM collecte des informations sur la visite du client et sur ses clics, ce qui peut être utile pour que les équipes de développement puissent analyser l’impact des nouvelles fonctionnalités.

Prise en main de RUM avec Elastic APM

Dans ce blog, nous allons étudier l’intégralité du processus pour instrumenter une application web simple, constituée d’un frontend React et d’un back-end Spring Boot, étape par étape. En ce qui concerne l’utilisation de l’agent RUM, c’est un vrai jeu d’enfants, vous allez voir ! En bonus, vous découvrirez comment Elastic APM met en conjonction les informations de frontend et de back-end sur les performances avec une vue de trace holistique et distribuée. Si vous souhaitez en savoir plus sur le traçage, consultez cet article de blog : Traçage distribué, OpenTracing et Elastic APM.

Pour utiliser la fonction RUM d’Elastic APM, vous devez disposer de la Suite Elastic avec un serveur APM. Vous pouvez, bien sûr, télécharger et installer la dernière version de la Suite Elastic avec un serveur APM en local sur votre ordinateur. Toutefois, pour vous faciliter la tâche, je vous conseille de créer un compte d’essai Elastic Cloud pour que votre cluster soit prêt en quelques minutes. APM est activé pour le modèle optimisé d’E/S par défaut. À partir de maintenant, je partirai du principe que vous disposez d’un cluster fonctionnel.

Exemple d’application

L’application que nous allons instrumenter est une application de base de données automobile simple, constituée d’un frontend React et d’un back-end Spring Boot, qui fournit un accès API à une base de données automobile in-memory. L’application reste simple à dessein. L’idée, c’est de vous montrer les étapes détaillées de l’instrumentation depuis le départ, pour que vous puissiez instrumenter vos propres applications en reproduisant cette procédure.

A simple application with a React frontend and Spring backend

Créez un répertoire appelé CarApp à l’emplacement que vous souhaitez sur votre ordinateur. Ensuite, clonez l’application frontend et back-end dans ce répertoire.

git clone https://github.com/carlyrichmond/carfront
git clone https://github.com/carlyrichmond/cardatabase

Comme vous pouvez le constater, l’application est extrêmement simple. Il y a seulement quelques composants dans le frontend React et quelques classes dans le back-end Spring Boot. Concevez et exécutez l’application en suivant les instructions dans GitHub concernant le frontend et le back-end. Vous devriez avoir une présentation similaire à celle ci-dessous. Vous pouvez parcourir les véhicules, les filtrer, ou encore les créer, les lire, les mettre à jour et les supprimer.

The simple React user interface

Maintenant que l’application est fonctionnelle et qu’elle est en cours d’exécution, nous pouvons étudier la procédure d’instrumentation à l’aide de l’agent RUM.

Instrumentation enrichie prête à l’emploi avec RUM

Un serveur Elastic APM est nécessaire pour commencer. Vous allez devoir activer RUM pour enregistrer les événements de votre agent RUM. Il y a deux manières de configurer l’agent RUM :

  1. Vous pouvez installer l’agent RUM comme dépendance du projet via un gestionnaire de packages comme npm :
    npm install @elastic/apm-rum --save
  2. Incluez l’agent RUM via la balise de script HTML. Notez que cette opération peut être effectuée comme opération bloquante ou non bloquante conformément à la documentation.
    <script 
    src="https://unpkg.com/@elastic/apm-rum@5.12.0/dist/bundles/elastic-apm-rum.umd.min.js">
    </script>
    <script>
    elasticApm.init({
    serviceName: 'carfront',
    serverUrl: 'http://localhost:8200',
    serviceVersion: '0.90'
    })
    </script>

Étant donné que notre frontend est une application React, nous opterons pour la première approche. Une fois que vous avez installé @elastic/apm-rum dans votre projet, vérifiez le code d’initialisation dans rum.js. Il se trouve dans le même répertoire que le fichier index.js et ressemblera un peu à ce qui suit, mais avec la valeur serviceUrl remplacée par votre propre point de terminaison de serveur APM :

import { init as initApm } from '@elastic/apm-rum'
var apm = initApm({
//Définissez le nom de service requis (caractères autorisés : a-z, A-Z, 0-9, -, _ et espace)
serviceName: 'carfront',
// Définissez la version de votre application
// Utilisée par le serveur APM pour trouver la source map appropriée
serviceVersion: '0.90',
// Définissez l’URL personnalisée du serveur APM (par défaut : http://localhost:8200)
serverUrl: 'APM_URL',
// distributedTracingOrigins: ['http://localhost:8080'],
})
export default apm;

Et voilà ! Nous venons d’initialiser l’agent RUM ! Si vous utilisez des fonctionnalités spécifiques au framework, par exemple le routage dans React, Angular ou Vue, vous pouvez aussi installer et configurer les intégrations spécifiques à ce framework, ce qui est expliqué dans la documentation. Dans ce cas, comme il s’agit d’une page simple qui ne nécessite pas d’instrumentation React spécifique, nous n’avons pas installé de dépendances supplémentaires.

Ne vous préoccupez pas de distributedTracingOrigins pour le moment. Voici une explication rapide de quelques-unes des autres configurations :

  1. serviceName : Le nom de service doit être défini. C’est lui qui représente votre application dans l’interface utilisateur APM. Donnez-lui un nom représentatif.
  2. serviceVersion : Il s’agit de la version de votre application. Ce numéro de version est également utilisé par le serveur APM pour trouver la source map appropriée. Nous aborderons les source maps plus en détail ultérieurement.
  3. serverURL : Il s’agit de l’URL du serveur APM. Remarque : L’URL du serveur APM est normalement accessible depuis Internet en mode public, car votre agent RUM y rapporte les données à partir des navigateurs des utilisateurs finaux sur Internet.

Les utilisateurs qui ont l’habitude des agents de back-end Elastic APM peuvent se demander pourquoi le token APM ne sert pas ici. La raison est toute simple : en réalité, l’agent RUM n’utilise pas de token APM secret. Le token sert uniquement pour les agents back-end. Étant donné que le code frontend est public, le token secret n’apportera pas de sécurité supplémentaire.

Nous allons charger ce fichier JavaScript lorsque l’application se chargera et nous le mettrons aux emplacements auxquels nous souhaitons réaliser une instrumentation personnalisée. Pour l’instant, étudions ce que ce fichier nous offre de base, sans instrumentation personnalisée. Pour cela, incluons tout simplement rum.js dans index.js. Le fichier index.js importe rum.js et définit un nom de chargement de page. Si aucun nom de chargement de page n’était défini, le chargement serait affiché comme "/" (Inconnu) dans l’interface utilisateur APM, ce qui ne serait pas très intuitif. Voici à quoi ressemble index.js.

import apm from './rum'
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
apm.setInitialPageLoadName("Car List")
ReactDOM.render(, document.getElementById('root'));
serviceWorker.unregister();

Générez du trafic sur votre application en accédant aux pages et en ajoutant ou en supprimant des véhicules. Ensuite, connectez-vous à Kibana et cliquez sur la vignette Observability. De là, sélectionnez l’option Services dans le sous-menu APM, comme illustré ci-dessous :

Vous devriez voir un service répertorié avec le nom "carfront". Si vous cliquez sur le nom de ce service, vous serez redirigé vers la page de la transaction. Vous devez voir une vue d’ensemble des indicateurs tels que la latence et le débit pour la période par défaut "Last 15 minutes" (15 dernières minutes). Sinon, changez le sélecteur de date pour afficher cette plage.

Dans le segment des transactions, vous devriez voir la transaction "Car List" (Liste des véhicules). Cliquez sur le lien "Car List" (Liste des véhicules) pour passer dans l’onglet Transaction, qui contient les statistiques de cet échantillon de transactions. Descendez jusqu’en bas de la page pour obtenir une vue en cascade des interactions avec le navigateur, comme dans l’exemple ci-dessous :

Vous avez vu toutes ces informations que l’agent RUM propose par défaut ? Impressionnant, n’est-ce pas ? Prêtez une attention particulière aux marqueurs en haut, comme timeToFirstByte, domInteractive, domComplete et firstContentfulPaint. Survolez les points noirs pour voir les noms. Ils vous donneront des informations précieuses concernant la récupération de contenu et le rendu de navigateur de ces contenus. Gardez également un œil attentif sur l’ensemble des données de performances relatives au chargement des ressources que fournit le navigateur. Lorsque vous initialisez votre agent RUM sans apporter la moindre instrumentation personnalisée, vous obtenez déjà tous ces indicateurs sur les performances ! En cas de problème de performances, ces indicateurs vous permettent de déterminer facilement si le problème vient de la lenteur des services back-end, de la lenteur du réseau ou tout simplement de la lenteur du navigateur client. Le moins qu’on puisse dire, c’est que ça en jette !

Pour ceux qui auraient besoin d’un petit rappel, voici une explication rapide sur la façon dont les performances web sont mesurées à l’aide d’indicateurs. Gardez toutefois à l’esprit que, pour les frameworks modernes d’applications web comme React, ces indicateurs peuvent représenter uniquement la partie "statique" de la page web, en raison de la nature asynchrone de React. Par exemple, les contenus dynamiques peuvent être toujours en cours de chargement après domInteractive, comme vous le verrez plus tard.

  • timeToFirstByte est la durée pendant laquelle un navigateur patiente en attendant de recevoir la première information du serveur web qu’il a demandée. Cette durée dépend à la fois de la vitesse de traitement côté réseau et côté serveur.
  • domInteractive représente le moment juste avant que l’agent utilisateur définisse l’état de préparation du document en cours sur "interactive" (interactif), ce qui signifie que le navigateur a fini d’analyser l’ensemble du contenu HTML et que la construction DOM est terminée.
  • domComplete correspond au moment juste avant que l’agent utilisateur définisse l’état de préparation du document sur "complete" (terminé), ce qui signifie que la page et toutes ses sous-ressources (p. ex. images) ont terminé de se télécharger et sont prêtes. L’indication de téléchargement s’arrête.
  • firstContentfulPaint est le moment où le navigateur effectue le rendu du premier bit de contenu à partir de DOM. Il s’agit d’un jalon important pour les utilisateurs car il indique que la page est en cours de chargement.

Instrumentation personnalisée flexible

L’agent RUM fournit une instrumentation détaillée prête à l’emploi pour que votre navigateur puisse interagir, comme vous venez de le voir. Si besoin, vous pouvez également mettre en place une instrumentation personnalisée. Par exemple, étant donné que l’application React est une application à page unique et que la suppression d’un véhicule n’entraînera pas de chargement de page, par défaut, RUM ne capturera pas les données de ladite suppression de véhicule. Dans un cas comme celui-ci, nous pouvons recourir aux transactions personnalisées.

Dans la version actuelle (APM Real User Monitoring JavaScript Agent 5.x), les appels AJAX et les événements de clic sont enregistrés par l’agent et envoyés au serveur APM. La configuration des types d’interactions peut se faire via le paramètre disableInstrumentation.

Il est également possible d’ajouter vos propres instrumentations personnalisées pour fournir des traces plus intéressantes. Ceci peut s’avérer particulièrement utile pour le suivi des nouvelles fonctionnalités. Dans notre exemple d’application, le bouton "New Car" (Nouveau véhicule) de notre application frontend permet d’ajouter un nouveau véhicule à la base de données. Nous allons instrumenter le code de sorte à capturer les performances de l’ajout d’un nouveau véhicule. Ouvrez le fichier Carlist.js dans le répertoire des composants. Vous verrez le code suivant :

//Ajouter un nouveau véhicule
addCar(car) {
// Ajoutez les métadonnées du véhicule comme étiquettes de la transaction de clic RUM
var transaction = apm.startTransaction("Add Car", "Car");
transaction.addLabels(car);
fetch(SERVER_URL + 'api/cars',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(car)
})
.then(res => this.fetchCars())
.catch(err => console.error(err))
}
fetchCars = () => {
fetch(SERVER_URL + 'api/cars')
.then((response) => response.json())
.then((responseData) => {
this.setState({
cars: responseData._embedded.cars,
});
})
.catch(err => console.error(err));
// Terminez la transaction actuelle à la fin du rappel de la réponse
var transaction = apm.getCurrentTransaction()
if (transaction) transaction.end()
}

Le code a créé une transaction appelée "Add Car" (Ajouter un véhicule) de type "Car" (Véhicule). Il a ensuite balisé la transaction à l’aide du véhicule afin de fournir des informations contextuelles. Nous avons ensuite terminé la transaction de façon explicite à la fin de la méthode.

Ajoutez un nouveau véhicule à partir de l’interface utilisateur web de l’application. Cliquez sur l’interface utilisateur APM dans Kibana. Une transaction "Add Car" (Ajouter un véhicule) devrait être répertoriée. Sélectionnez "Car" (Véhicule) dans la liste déroulante "Filter by type" (Filtrer par type). Par défaut, ce sont les transactions de chargement de page ("page-load") qui sont affichées.

Cliquez sur le lien de transaction "Add Car" (Ajouter un véhicule). Des informations sur les performances de la transaction personnalisée "Add Car" (Ajouter un véhicule) devraient vous être présentées :

Cliquez sur l’onglet "Métadonnées". Vous verrez les étiquettes que nous avons ajoutées avec les étiquettes par défaut enregistrées par l’agent. Les étiquettes et les logs ajoutent des informations contextuelles précieuses à vos traces APM.

Voilà ! Pour réaliser une instrumentation personnalisée, c’est aussi simple que cela. Et en plus, vous optimisez votre puissance ! Pour en savoir plus, consultez la documentation sur l’API.

Tableau de bord de l’expérience utilisateur

Elastic APM propose une interface utilisateur APM optimisée et des tableaux de bord APM intégrés pour visualiser toutes les données APM capturées directement par les agents.

Vous pouvez également créer vos propres visualisations dans Elastic à l’aide de pipelines de nœuds d’ingestion afin d’enrichir et de transformer vos données APM. Par exemple, l’IP utilisateur et les données capturées par l’agent RUM sont des informations extrêmement riches concernant vos clients. Avec ces informations, il est possible de créer une visualisation comme celle-ci pour montrer d’où vient le trafic web sur une carte et quels sont les systèmes d’exploitation et les navigateurs utilisés par vos clients.

Cependant, la plupart des données intéressantes sur l’utilisateur peuvent être présentées dans le tableau de bord de l’expérience utilisateur, visible dans Elastic Observability. Voici des exemples de visualisations :

Obtenez une vue d’ensemble grâce au traçage distribué

En bonus, nous allons également instrumenter notre application Spring Boot back-end. Ainsi, vous aurez un aperçu complet de la transaction globale à partir du navigateur web jusqu’à la base de données back-end, le tout, en une seule vue. Pour y parvenir, nous nous servirons du traçage distribué APM.

Configuration du traçage distribué dans les agents RUM

Le traçage distribué est activé par défaut dans l’agent RUM. Toutefois, il inclut uniquement les requêtes effectuées d’une même origine. Pour prendre en charge les requêtes entre origines multiples, vous devez définir l’option de configuration distributedTracingOrigins. Vous devrez également configurer la politique CORS dans l’application back-end, ce que nous verrons dans la section suivante.

Pour notre application, le frontend est démarré à partir de http://localhost:3000. Pour inclure les requêtes effectuées vers http://localhost:8080, nous devons ajouter la configuration distributedTracingOrigins à notre application React. Pour cela, nous nous servons du fichier rum.js. Le code s’y trouve déjà. Supprimez tout simplement le commentaire de la ligne.

var apm = initApm({
...
distributedTracingOrigins: ['http://localhost:8080']
})

Les nouvelles versions de l’agent utilisent la spécification W3C Trace Context et l’en-tête transparent dans les requêtes envoyées à http://localhost:8080. Cependant, notez que ceci était auparavant fait par l’ajout de l’en-tête personnalisé elastic-apm-traceparent à ces requêtes.

D’après la documentation de la dernière version, l’instrumentation côté serveur peut être configurée de trois manières :

  1. En joignant automatiquement la JVM en cours d’exécution à l’aide de apm-agent-attach-cli.jar
  2. En configurant par programme à l’aide de apm-agent-attach, ce qui nécessite de modifier le code de votre application Java
  3. Avec une configuration manuelle à l’aide de l’indicateur -javaagent, comme nous allons le voir dans l’exemple qui suit

Pour utiliser l’approche avec instrumentation manuelle côté serveur, vous devez télécharger l’agent Java et vous en servir pour démarrer l’application. Dans votre environnement de développement, vous devez ajouter les éléments vmArgs suivants à la configuration du lancement.

-javaagent:apm/wrapper/elastic-apm-agent-1.33.0.jar 
-Delastic.apm.service_name=cardatabase
-Delastic.apm.application_packages=com.packt.cardatabase
-Delastic.apm.server_urls=
-Delastic.apm.secret_token=

Si vous utilisez Elastic Cloud, la configuration complète des agents RUM et APM se trouve dans l’intégration APM de votre déploiement, dont voici un exemple.

L’endroit dans lequel les agents sont configurés dépend de votre environnement de développement. La capture d’écran suivante vient de ma configuration de lancement VSCode pour l’application Spring Boot :

Maintenant, actualisez votre liste de véhicules à partir du navigateur pour générer une autre requête. Accédez à l’interface utilisateur APM de Kibana et vérifiez le dernier chargement de page "car list". vous devriez voir une trace complète avec les invocations de méthodes Java, comme dans la capture d’écran suivante :

Comme vous pouvez le voir, vos données de performances côté client et vos données de performances côté serveur, y compris l’accès JDBC, apparaissent toutes de façon bien organisée dans une trace distribuée unique. Notez que différentes couleurs ont été employées pour différentes parties de la trace distribuée. Gardez à l’esprit qu’il s’agit là du traçage par défaut et qu’il n’y a besoin d’aucune instrumentation personnalisée côté serveur, à part démarrer votre application avec l’agent. Bénéficiez de la puissance d’Elastic APM et du traçage distribué !

Aux lecteurs qui ont observé attentivement la visualisation chronologique ci-dessus : vous vous demandez peut-être pourquoi la transaction de chargement de page "Car List" prend fin à 193 ms, ce qui correspond à domInteractive, alors que les données sont toujours servies à partir du back-end. C’est une excellente question ! Cela s’explique par le fait que les appels d’extraction sont asynchrones par défaut. Le navigateur "pense" qu’il a fini d’analyser l’ensemble du contenu HTML et que la construction DOM est achevée à 193 ms du fait qu’il a chargé l’ensemble du contenu HTML "statique" servi à partir du serveur web. Par ailleurs, React continue à charger des données depuis le serveur back-end de façon asynchrone.

Partage des ressources entre origines multiples (CORS)

L’agent RUM n’est qu’un élément parmi d’autres constituant la trace distribuée. Pour pouvoir utiliser le traçage distribué, nous devons aussi configurer correctement les autres composants, notamment, le partage des ressources entre origines multiples, le "célèbre" CORS ! Pourquoi ? Car en général, les services de frontend et de back-end sont déployés séparément. Avec la politique concernant l’origine unique (same-origin), les requêtes frontend issues d’une origine différente de celle du back-end échoueront si le CORS n’est pas bien configuré. En quelques mots, le CORS est une méthode qui permet, côté serveur, de vérifier si les requêtes venant d’une autre origine sont autorisées. Pour en savoir plus sur les requêtes aux origines multiples et pour comprendre la nécessité de ce processus, consultez la page MDN sur le partage des ressources entre origines multiples.

Concrètement, qu’est-ce que cela signifie pour nous ? Deux choses, que voici :

  1. Nous devons définir l’option de configuration distributedTracingOrigins, comme nous l’avons fait.
  2. Avec cette configuration, l’agent RUM envoie une requête HTTP OPTIONS avant la requête HTTP réelle afin de vérifier que tous les en-têtes et toutes les méthodes HTTP sont bien pris en charge et que l’origine est autorisée. Plus précisément, http://localhost:8080 recevra une requête OPTIONS avec les en-têtes suivants :
    Access-Control-Request-Headers: traceparent, tracestate
    Access-Control-Request-Method: [request-method]
    Origin: [request-origin]
    Et le serveur APM devrait y répondre avec les en-têtes ci-dessous et un état de code 200 :
    Access-Control-Allow-Headers: traceparent, tracestate
    Access-Control-Allow-Methods: [allowed-methods]
    Access-Control-Allow-Origin: [request-origin]

C’est exactement l’effet qu’a la classe MyCorsConfiguration dans notre application Spring Boot. Différentes méthodes sont possibles pour configurer l’application Spring Boot afin qu’elle agisse ainsi. Ici, nous utilisons une approche basée sur des filtres. Résultat : notre application Spring Boot côté serveur autorise les requêtes venant de n’importe quelle origine, quels que soient les en-têtes HTTP et les méthodes HTTP utilisés. Vous ne voudrez peut-être pas cependant avoir une telle flexibilité dans vos applications de production.

@Configuration
public class MyCorsConfiguration {
@Bean
public FilterRegistrationBean corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(0);
return bean;
}
}

Résumé

Comme nous l’avons vu, il est extrêmement simple de réaliser l’instrumentation de vos applications avec Elastic RUM. Et très puissant. Combiné à d’autres agents APM pour les services back-end, RUM vous donne une vue holistique des performances d’une application du point de vue de l’utilisateur final grâce au traçage distribué.

Comme déjà mentionné, pour vous lancer sur Elastic APM, vous pouvez télécharger le serveur Elastic APM pour l’exécuter en local, ou créer un compte d’essai Elastic Cloud pour avoir un cluster prêt en quelques minutes.

Comme toujours, rendez-vous sur leforum Elastic APM si vous souhaitez ouvrir un fil de discussion ou si vous avez des questions. Profitez bien !

Cet article a été initialement publié le 1er avril 2019. Il a été mis à jour le 20 octobre 2022.