Savez-vous ce que vous avez en production ? Lorsque votre application transmet un log, savez-vous depuis quel fichier source provient-il et surtout à quel commit est-il rattaché ?... Si toutefois, vous avez bien pensé à intégrer sous Git les dernières modifications...

Savoir d'où provient un appel dans le code est une nécessité. Savoir à quel commit est rattaché la ligne de code qui a déclenché l'appel est aussi une nécessité. D'une manière générale, plus il y aura d'informations fournies sur le contexte de production d'une ligne de code, plus il sera aisé de comprendre l'intention autour du déclenchement d'un appel et accessoirement de déboguer une application.

Nous sommes en 2019... Malgré ça, nous allons continuer à voir des "patchs" fait main et non versionnés, passer hors des contrôles des serveurs d'intégration continu, pour aller se loger bien au chaud dans vos serveurs de production ou ceux de vos clients 🐛.

L'idée derrière notre projet cause-toujours est de fournir ces informations autour du lieu d'appel (call site en anglais) et de fournir ce contexte manquant lorsqu'un soucis est remonté dans vos équipes.

cause-toujours se base d'abord sur une structure de donnée permettant de conserver un ensemble de méta-données sur le site d'appel. Cette structure est nommée CallSiteInfo.

case class CallSiteInfo(
    enclosingClass: String, // name of the enclosing unit
    file: String, // file for the callsite (relative to the git repo root)
    line: Int, // line number in file
    commit: String, // id of the commit
    buildAt: Long, // time you build at
    status: String, // one of "clean", "modified", "untracked"
    fingerprint: String, // file content hash
    fileContent: Option[String] = None // if the build file is different
)

Des types primitifs sont utilisés ici afin d'assurer la sérialisation de la structure et de faciliter son interprétation par des applications basées sur d'autres langages.

La récupération de ces informations se fait au moment de la compilation du code source. Scala propose en effet un système de macro syntaxique pour ça. Une macro Scala est un morceau de code qui intervient au moment de la compilation, lorsque l'arbre syntaxique (AST - Abstract Syntax Tree) a été formé. Cette macro va permettre d'effectuer des transformations dans l'AST. C'est un peu comme de la génération de code, mais dans laquelle la syntaxe du langage est forcément respectée.

La déclaration d'une macro se fait en utilisant le mot-clé correspondant.

import language.experimental.macros

implicit def callSiteInfo: CallSiteInfo = macro CallSiteMacro.callSiteImpl

Ainsi, à chaque fois que callSiteInfo apparaît dans le code, le compilateur donne la main à la fonction CallSiteMacro.callSiteImpl. Les macros dans Scala restent une fonctionnalité expérimentale. Comme expliqué lors d'un précédent article, dont le sort devrait être fixé avec Scala 3, qui devrait sortir en 2020.

import scala.reflect.macros.blackbox

def callSiteImpl(c: blackbox.Context): c.Expr[CallSiteInfo] = {
  import c._
  import universe._

  // ...

  c.Expr[CallSiteInfo](
    q"""callsite.CallSiteInfo(
  enclosingClass = ${owner.fullName},
  file           = ${pathToRepoRoot(sourceFile)},
  ...
  )""")
}

Le paramètre de type blackbox.Context correspond au contexte au niveau de l'AST dans lequel est appelé la macro. Blackbox signifie ici que la macro agira dans un contexte dans lequel toute résolution de type est complète. La macro retourne une expression de type CallSiteInfo, qui est formalisée par les dernières lignes de la macro en utilisant une quasiquote. Une quasiquote se présente comme une chaîne de caractères contenant du code Scala. Ce code est ensuite converti en AST et intégré dans l'AST de votre application. C'est ainsi qu'il est possible de récupérer la date de compilation du code. À partir de l'API scala.reflect, il est possible d'obtenir le fichier, le numéro de ligne ou le code source dans lequel se fait le site d'appel. Et en utilisant JGit (org.eclipse.jgit), il est aussi possible de récupérer le commit ID ou d'indiquer si le fichier est dans une version conservé sous Git.

On peut ainsi imaginer écrire une fonction de log de la manière suivante :

def log(message: String)(implicit cs: callsite.CallSiteInfo): Unit = {
  println(s"[${cs.commit.take(7)} - ${cs.file}:${cs.line} @${cs.date}] $message")
  cs.fileContent.foreach(println)
}

Il n'y a pas besoin d'importer callsite.CallSiteInfo. Et comme le paramètre cs est implicite, il n'y a pas besoin de le renseigner lors de l'appel à log. Voici un exemple d'utilisation :

import /*...*/.log

object MyMain {
  def main(args: Array[String]): Unit = {
    println("Hello printed")
    log("Hello logged")
  }
}

Si ce code est stocké dans un fichier appelé MyMain.scala et que ce fichier n'a pas été modifié depuis le dernier commit Git, on aura alors l'affichage suivant :

Hello printed
[b834c88 - MyMain.scala:6 @946684800] Hello logged

cause-toujours est utilisé dans le cadre de notre projet Zoom. Zoom permet de centraliser, de réorganiser et de redistribuer des flux de données dans un SI. Ce qui inclut aussi les flux de logs.