Article écrit par Hugo F.
Enrichir les événements Sentry
L’usage basique de Sentry (ou outil équivalent) est répandu et bien connu de la plupart des développeurs.
Seulement, il arrive que certains bugs se produisent dans un contexte assez particulier où simplement recevoir une alerte avec le code incriminé ne suffise pas forcément à trouver la source.
L’idée de cet article est de présenter certaines (parce qu’il y en a beaucoup et différentes selon les SDK) fonctionnalités mises en place par Sentry pour nous aider.
Pour ma part, les exemples seront en Rails avec le SDK officiel, mais je pense que toutes les fonctions que nous allons voir sont disponibles dans tous les SDK.
Prérequis
Je ne vais rien présenter de très complexe, mais il est important de bien comprendre la base du fonctionnement de Sentry.
Je ne vais pas rentrer dans les détails parce que ça serait paraphraser la documentation officielle.
Pour Sentry tout se base sur le concept de Hub
et de Scope
. Le Hub
, que nous ne toucherons pas, correspond à la brique principale qui contient les informations pour envoyer les évènements au bon serveur Sentry plus le Scope
qui entoure l’événement courant.
Un Scope
(un contexte donc) correspond aux informations additionnelles qui vont pouvoir être liées à un événement. Globalement, c’est tout ce qu’on retrouvera dans l’interface sur la page d’un événement :
Setup
Je vais faire une toute petite passe sur le setup mais il s’agit simplement de suivre la documentation.
Je pars d’un simple projet Rails généré en mode API:
rails new sentry_article --api
Puis on ajoute ce qu’il faut pour faire fonctionner Sentry
# Gemfile
gem "sentry-ruby"
gem "sentry-rails"
# config/initializers/sentry.rb
Sentry.init do |config|
config.dsn = ENV.fetch("SENTRY_DSN")
config.breadcrumbs_logger = [:active_support_logger, :http_logger]
# Ici on met à 1.0 (100%) pour s'assurer de récupérer tout les events
# Dans le cas d'une application à fort trafic on voudra probablement l'ajuster
config.traces_sample_rate = 1.0
end
Pour la suite de l’article, je vais utiliser un controller HomeController
qui sera enrichi par différentes actions. Voilà les routes qui seront utilisées :
# config/routes.rb
Rails.application.routes.draw do
get "home/error", to: "home#error"
get "home/context/:msg", to: "home#context"
get "home/tags/:msg", to: "home#tags"
get "home/attachment", to: "home#attachment"
get "home/scope/:msg", to: "home#scope"
end
Et ensuite j’utilise mon navigateur pour taper sur les différents endpoints.
Les problèmes
Pour tester l’intégration Sentry, le mieux est de simuler une erreur, c’est à ça que nous sert l’action erreur :
# app/controllers/home_controller.rb
class HomeController < ApplicationController
def error
raise "Error"
end
end
Personnaliser son contexte
Contexte global
Dans certains cas, on pourrait avoir besoin de garder un contexte global (plusieurs instances de l’application pour un seul Sentry par exemple) On peut simplement les spécifier dans l’initializer dédié :
# config/initializers/sentry.rb
Sentry.init do |config|
# ...
end
Sentry.configure_scope do |scope|
scope.set_context("custom_global_meta", { instance_name: ENV.fetch("INSTANCE_NAME", "Article sur Sentry") })
end
Ici mon contexte custom_global_meta
sera remonté pour chaque événement.
Contexte local
Je vais partir d’une problématique que j’ai rencontrée :
J’ai une tâche qui tourne assez régulièrement qui va scanner un bucket s3 à la recherche de nouveau fichiers et ingérer les données de ces fichiers dans la base de données.
Rien de très complexe dans l’idée, mais on ne peut jamais être sûr que tout se passe bien parce que nous ne sommes pas maîtres des fichiers (et donc de leur format/contenu/encodage/…). Il peut donc y avoir régulièrement des soucis.
Heureusement, Sentry nous permet repérer ces crashs et de les traiter ou de faire remonter le problème.
Mais pour mieux identifier ce qu’il se passe, il est très pratique de pouvoir rajouter du contexte aux évènements Sentry.
Dans mon cas, j’aimerai connaitre le nom du fichier qui a causé l’erreur.
Pour ajouter du contexte on peut modifier le Scope
courant pour lui rajouter des informations :
# app/controllers/home_controller.rb
class HomeController < ApplicationController
def context
Sentry.configure_scope do |scope|
scope.set_context("meta", { msg: params[:msg] })
end
raise "Context"
end
end
Et si on regarde la section Environment
de l’événement sur Sentry on retrouvera bien notre contexte (ainsi que le contexte global qu’on a mis plus haut :
On a déjà pu récupérer une information qui peut être très pratique, mais maintenant je voudrais pouvoir rechercher tous les événements qui ont été levés par un fichier donné, comment faire ?
Il faut savoir que l’ajout de contexte structuré via Sentry n’est pas indexé par la plateforme. Par conséquent, impossible de se servir de ces informations pour filtrer les issues.
Heureusement, Sentry nous met à disposition un autre outil, les tags qui sont eux bien indexés.
L’utilisation des tags ne pourrait pas être plus simple (attention, il existe des tags par défaut utilisés par Sentry d’où le préfixe custom.
):
# app/controllers/home_controller.rb
class HomeController < ApplicationController
def tags
Sentry.set_tags("custom.request.message": params[:msg])
raise "Tags"
end
end
On retrouve bien notre tag custom.request.message
dans la section dédiée.
On peut maintenant facilement chercher avec notre nom de fichier toutes les erreurs liées :
Avec ces deux outils, on est plutôt bien équipé pour retrouver toutes les informations dont on a besoin pour mieux cibler l’erreur.
Mais encore une fois, dans mon cas, ne pourrait-on pas aller plus loin ?
En effet, ça serait assez pratique pour le développeur qui doit régler le bug en question de pouvoir retrouver le fichier qui a causé un bug lors de l’import pour reproduire simplement le problème sur son poste ?
Eh bien c’est possible et même assez simple :
# app/controllers/home_controller.rb
class HomeController < ApplicationController
def attachment
Sentry.configure_scope do |scope|
scope.add_attachment(path: Rails.root.join("README.md"))
# On peut aussi envoyer directement le contenu du fichier
# scope.add_attachment(filename: "README.md", bytes: Rails.root.join("README.md").read))
end
raise "Attachment"
end
end
Il existe d’autres moyens d’enrichir encore plus les événements remontés par Sentry. Ne les ayant pas encore utilisés je ne ferai que reprendre la documentation, je vous encourage donc plutôt à aller vous renseigner si vous souhaitez en savoir plus.
Choisir entre Sentry.configure_scope
et Sentry.with_scope
attention with_scope
peut avoir d’autres noms ou usages selon les SDK
En effet, si à l’usage ces deux fonctions sont très similaires, elles ne répondent pas au même besoin.
La première va enrichir le contexte courant, donc si on enchaine les appels à configure_scope
, les informations vont s’ajouter. En revanche, with_scope
va cloner le contexte courant, l’enrichir avec les informations ajoutées mais ne viendra pas enrichir notre contexte courant, donc si une erreur survient en dehors du bloc, les informations ajoutées par with_scope
seront perdues.
Pour bien comprendre les différences entre ces deux fonctions, le mieux, c’est de les tester.
# app/controllers/home_controller.rb
class HomeController < ApplicationController
def scope
Sentry.configure_scope do |scope|
scope.set_context("custom_scopped_meta", { msg: params[:msg] })
end
case params[:msg]
when "with"
Sentry.with_scope do |scope|
scope.set_context("custom_local_meta", { msg: "From the with !" })
raise "Scope from with"
rescue => e
Sentry.capture_exception(e)
raise e
end
when "configure"
Sentry.configure_scope do |scope|
scope.set_context("custom_scopped_meta", { warn: "context are stacked" })
end
raise "Scope from configure"
else
Sentry.with_scope do |scope|
scope.set_context("custom_local_meta", { msg: "From the else !" })
raise "Scope from else"
end
end
end
end
Ici si on fait un appel à l’action avec configure
comme paramètre, on retrouvera bien comme attendu notre contexte enrichi avec le msg
à configure
et le warn
.
En revanche le piège se trouve dans le else
, en effet si on passe autre chose que configure
ou with
, on se retrouve avec le contexte uniquement enrichi de la première meta (la valeur du msg
), alors qu’en lisant simplement la documentation, on pourrait s’attendre à retrouver notre From the else !
.
L’explication vient du fait qu’on soit habitué à ce que Sentry attrape par lui-même les erreurs pour ensuite envoyer les événements.
Le souci, c’est que ce processus vient en fait d’un mécanisme global (qui n’est pas inhérent à Sentry tout seul) et donc tout ça survient hors du bloc, on perd donc notre scope qui est uniquement local au bloc.
Pour passer outre ce fonctionnement, il est nécessaire de capturer l’exception à la main, comme c’est fait si on passe le paramètre with
, dans ce cas, on retrouve bien notre meta local :
Le mot de la fin
Comme la majorité des outils qu’on utilise au quotidien, on part facilement du principe qu’on sait s’en servir et on ne va plus forcément aller approfondir le sujet ni se tenir au courant des mises à jour. C’est dommage parce qu’on passe à côté de fonctionnalités qui pourraient nous permettre d’améliorer encore plus notre façon de travailler. En bref, n’hésitez pas à lire les changelogs et la documentation !