23 juin 2025

Débogage en production avec les agents Java

Design & Code

Photo collaborateur.

Rihab

Dbouki

photo ordinateur.

23 juin 2025

Débogage en production avec les agents Java

Design & Code

Photo collaborateur.

Rihab

Dbouki

photo ordinateur.

23 juin 2025

Débogage en production avec les agents Java

Design & Code

Photo collaborateur.

Rihab

Dbouki

photo ordinateur.

Lors de la 13ᵉ édition de Devoxx France, qui s’est tenue au Palais des Congrès le 18 avril 2025, l’ingénieur logiciel Jean-Philippe Bempel a présenté une conférence intitulée « Comment déboguer en production ? » — une exploration audacieuse et résolument pragmatique de la résolution de bugs réels, ceux qui n’apparaissent qu’en conditions de production.

Plutôt que de considérer le débogage en production comme un dernier recours, Bempel a invité le public à le voir comme un pilier fondamental de l’observabilité et de la résilience.

Pourquoi déboguer en production ?

« Certains bugs n’apparaissent qu’en production. »

Les environnements de test ou de préproduction échouent souvent à reproduire des problèmes critiques, car ils n’intègrent pas :

  • Des volumes de trafic réels

  • Des données malformées ou "sales"

  • Des intégrations tierces imprévisibles

  • Des défaillances non déterministes comme les conditions de course

Dans ces cas, le débogage en production n’est pas seulement utile — c’est souvent la seule solution.


Les pipelines CI/CD sont trop lents pour le débogage

Bempel a souligné à quel point même une simple modification — comme l’ajout d’un log — devient extrêmement lente lorsque les pipelines CI/CD nécessitent :

  • Modification du code

  • Commit & revue de code

  • Construction CI

  • Déploiement en staging

  • Validation

  • Déploiement en production

« Ajouter une simple ligne de log peut prendre des heures. Il nous faut des outils pour observer la production sans modifier le code. »

C’est là qu’intervient l’API instrumentation de la JVM.

CI CD


Instrumentation JVM & Agents Java

Bempel a mis en avant l’API d’instrumentation, introduite dans JDK 1.5, qui permet aux agents dynamiques Java de modifier ou d’inspecter le comportement d’une application à l’exécution, sans modifier le code source ni redémarrer le service.


Comment fonctionne un agent Java ?

Voici le cycle de vie simplifié d’un agent Java :

Démarrage de la JVM

  ├── Chargement de l’agent (-javaagent:agent.jar)

  │     ├── Appel de premain()

  │     └── Enregistrement d’un ClassFileTransformer

  ├── Chargement des classes

  │     └── Le transformateur modifie le bytecode

  └── Exécution de main() de l’application

Il est aussi possible d’attacher dynamiquement un agent à une JVM déjà en cours d’exécution via agentmain() et des outils comme com.sun.tools.attach.VirtualMachine.


Example: API d’instrumentation en action

public class Agent {
	public static void premain(String agentArgs, Instrumentation inst) {
		System.out.println("Agent initialized.");
		inst.addTransformer(new LoggingClassTransformer());
	}
}

 

public class LoggingClassTransformer implements ClassFileTransformer {
  public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
          ProtectionDomain protectionDomain, byte[] classfileBuffer) {
      if (className == null || !className.contains("MyService")) return null;
      try {
          ClassPool cp = ClassPool.getDefault();
          CtClass cc = cp.get("com.example.MyService");
          CtMethod m = cc.getDeclaredMethod("process");
          m.insertBefore("System.out.println(\"[DEBUG] Entering process()\");");
          return cc.toBytecode();
      } catch (Exception e) {
          e.printStackTrace();
      }
      return null;
  }
}

Lancement avec :

java -javaagent:agent.jar -jar yourApp.jar


Manipulation de bytecode : ASM vs ByteBuddy

Pour modifier dynamiquement le bytecode, Bempel a présenté deux bibliothèques :

ASM

Une boîte à outils bas niveau utilisée par des frameworks comme Javassist, Mockito ou Kotlin. Elle propose :

  • Core API : Performances maximales, faible abstraction

  • Tree API : Abstraction plus élevée, transformations plus simples

👉 Utiliser Core API pour les performances, Tree API pour la flexibilité.

 

ByteBuddy

ByteBuddy est une bibliothèque haut niveau bâtie sur ASM, avec une API fluide. Idéale pour créer des agents sans écrire de bytecode brut.

Exemple avec ByteBuddy :

public class TracingInterceptor {    
  @RuntimeType    
  public static Object intercept(@SuperCall Callable<?> zuper) throws Exception {        
    System.out.println("[ByteBuddy] Before method call")

new AgentBuilder.Default()
  .type(named("com.example.MyService"))
  .transform((builder, td, cl, jm) ->        
    builder.method(named("process"))
      .intercept(MethodDelegation.to(TracingInterceptor.class))
).installOn(instrumentation);

 

Construire un débogueur de production avec des Spans

Bempel a montré comment créer un débogueur léger en injectant dynamiquement des Spans, inspiré du traçage distribué (ex : OpenTelemetry).

Spans : traçage d’exécution

Un Span enregistre :

  • L’heure de début et de fin

  • Le nom de l’opération

  • Le contexte (thread, classe, tags)

  • Les liens hiérarchiques (parent/enfant)


Cela permet de reconstituer le parcours d’exécution sans modifier la logique métier.

public class SpanInterceptor {
  @RuntimeType    
  public static Object trace(@SuperCall Callable<?> zuper, @Origin Method method) throws Exception {
    Span span = Span.start(method.getName());
    try {
      return zuper.call();
    } finally {
      span.end()

Ajouter des métriques en production

Au-delà du traçage, Bempel a également montré comment injecter dynamiquement des métriques dans une application en production à l’aide de MeterProvider d’OpenTelemetry.

« Pas besoin de redéploiement pour compter combien de fois une méthode est appelée — injectez la métrique. »

Exemple d’injection de métrique :

import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.api.metrics.MeterProvider;
import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributeKey;

public class MetricsInterceptor {
  private static final Meter meter = MeterProvider.getDefault().get("prod-debugger");
  private static final LongCounter counter = meter        
    .counterBuilder("method.calls")        
    .setDescription("Counts method invocations")        
    .setUnit("calls")        
    .build();    

@RuntimeType    
public static Object intercept(@SuperCall Callable<?> zuper, @Origin Method method) throws Exception {
  counter.add(1, Attributes.of(AttributeKey.stringKey("method"), method.getName()))

Cela permet d’avoir une vue en temps réel sur la fréquence des appels, les erreurs ou la latence — sans redéploiement.


Conclusion

Le débogage en production n’est plus un luxe, c’est une nécessité. Certains bugs ne peuvent pas être reproduits dans un environnement de test ou de staging à cause des différences de trafic, de données ou de comportement concurrent.

Les pipelines CI/CD sont trop rigides pour répondre aux besoins d’analyse rapide. Grâce à l’API d’instrumentation de la JVM et aux agents Java, il est désormais possible de modifier le bytecode en temps réel, sans redémarrage ni changement de code source.

Bempel a présenté des outils concrets comme ASM et ByteBuddy, permettant une instrumentation dynamique, un traçage fin avec des Spans, et une collecte de métriques en production via OpenTelemetry. Ces techniques ouvrent la voie à un débogueur sur mesure, tournant aux côtés de l’application, offrant des insights puissants sans impacter la performance ni la disponibilité.