Dirac s’oppose à l’instrumentation par réflexion à chaud : tout le câblage est fait au démarrage du container CDI, et le chemin chaud — incrémenter un compteur, mesurer une durée — n’engage ni verrou, ni allocation parasite, ni synchronized. Cette page décrit la séquence exacte et les invariants de threading.

Séparation dirac-core / dirac-cdi-vauban

dirac-core est du Java pur. Il ne connaît ni CDI, ni JAX-RS — uniquement la spec MP Metrics. Les types CounterImpl, GaugeImpl, HistogramImpl, TimerImpl, MetricRegistryImpl, BaseMetricsRegistrar, OpenMetricsFormatter et JsonMetricsFormatter sont testables unitairement sans container, et utilisables hors CDI (un outillage Maven ou un test JMH peut les consommer directement).

dirac-cdi-vauban est la couche d’intégration : intercepteurs @Counted et @Timed, BCE DiracExtension qui valide et résout les @Gauge, producer MetricRegistryProducerBean qui expose les trois MetricRegistry.

dirac-rest est la couche HTTP optionnelle : MetricsEndpoint consomme les registres et délègue à un formatter selon Accept.

Pipeline de démarrage

Diagram
  1. DiracExtension est découverte par Vauban via le ServiceLoader (provides BuildCompatibleExtension).

  2. Phase @Enhancement — collecte des classes annotées @Gauge ; chaque méthode est résolue en MethodHandle (privilégier MethodHandles.privateLookupIn plutôt que setAccessible).

  3. Phase @Synthesis — synthèse du MetricRegistryProducerBean @ApplicationScoped, propriétaire des trois registres.

  4. À l’activation du container, @PostConstruct du producer instancie les MetricRegistryImpl et fait appel à BaseMetricsRegistrar pour peupler le scope BASE (JVM gauges, GC, threads, heap).

  5. Les Gauge collectées sont enregistrées comme Gauge<T> dans le scope cible (APPLICATION par défaut).

Aucune classe utilisateur n’est instanciée tant qu’elle n’est pas effectivement injectée : la résolution MethodHandle est faite sur la métadonnée, pas sur l’instance.

Chemin chaud @Counted

L’intercepteur CountedInterceptor est dispatché par CDI à chaque invocation. Il maintient un cache ConcurrentHashMap<CacheKey, ResolvedCounter> indexé par (BeanClass, Method) — la résolution MetricID n’a lieu qu’une fois, au premier appel ; les suivants tirent du cache.

// extrait simplifié de CountedInterceptor.aroundInvoke
ResolvedCounter resolved = counters.computeIfAbsent(
    new CacheKey(beanClass, method),
    k -> resolve(beanClass, method));
resolved.counter.inc();        // LongAdder — pas de contention
return ctx.proceed();

Counter.inc() est un LongAdder.increment() — pas de CAS, pas de synchronized. Les Tag sont des record immuables.

TimedInterceptor suit le même schéma : long t0 = System.nanoTime(); avant ctx.proceed(), histogram.update(System.nanoTime() - t0); après.

Registre MetricRegistryImpl

Trois invariants :

  1. ConcurrentHashMap par scope — clé MetricID, valeur Metric. Insertion via computeIfAbsent, lecture lock-free.

  2. Idempotencecounter(metadata, tags) retourne la métrique existante si elle est déjà enregistrée. Pas de doublon possible sur un MetricID.

  3. Pas de synchronized — l’enregistrement et la lecture passent uniquement par la ConcurrentHashMap. Pas de barrière mémoire critique.

Le registre est @ApplicationScoped : une instance unique par scope, propriété de MetricRegistryProducerBean.

HistogramImpl et percentiles

HistogramImpl utilise trois primitives sans verrou :

  • LongAdder count — compteur d’échantillons.

  • LongAdder sum — somme des valeurs.

  • LongAccumulator max(Long::max, Long.MIN_VALUE) — maximum.

  • ConcurrentLinkedQueue<Long> values — buffer pour calcul de percentiles à la lecture.

Les percentiles sont calculés à la demande par le formatter (tri du buffer puis interpolation), pas à chaque update. Coût d’instrumentation : un add sur une queue lock-free + trois increments d'`adder`.

Les buckets fixes (mp.metrics.distribution.histogram.buckets) ajoutent une famille _bucket{le=…​} à l’export OpenMetrics, calculée par parcours du buffer.

TimerImpl

TimerImpl enveloppe un HistogramImpl (avec timerMetric=true pour activer le profil de buckets timer). La méthode time(Runnable) mesure via System.nanoTime(). Pas de ThreadLocal, pas de Stopwatch thread-bound : la mesure se fait sur la pile de l’appel.

Format OpenMetrics

OpenMetricsFormatter écrit directement dans un OutputStream via StringBuilder et getBytes(UTF_8). Aucune librairie tierce, aucun moteur de templating.

Convention : pour les Timer, le type émis est summary (lignes _sum, _count, quantile), conforme à https://openmetrics.io. Pour les Histogram avec buckets, le type est histogram (lignes _bucket{le=…​}, _sum, _count).

Format JSON

JsonMetricsFormatter produit l’arborescence MP Metrics §3.2 — un objet par scope, contenant un objet par famille de métrique. Le sérialiseur est écrit à la main avec StringBuilder (échappement minimal des chaînes, pas de pretty-print) — dans l’esprit de Champollion, sans Jackson ni Yasson.

Modèle de threading

Pas de verrou à chaud

Le chemin chaud — inc(), update(long), time(…​) — n’engage ni synchronized, ni ReentrantLock. Les structures concurrentes (LongAdder, LongAccumulator, AtomicLongArray, ConcurrentHashMap, ConcurrentLinkedQueue) sont conçues pour la concurrence forte sans cession au scheduler.

Pas de ThreadLocal

Conformément au CLAUDE.md du module, aucun ThreadLocal n’est utilisé en production. Quand un partage d’état est nécessaire, les structures concurrentes ci-dessus sont préférées.

Virtual threads

Sur un runtime hôte qui exécute sur virtual threads (par exemple Chappe avec Executors.newVirtualThreadPerTaskExecutor()), aucune contention plateforme n’est introduite par Dirac : les LongAdder et ConcurrentHashMap sont indifférents à la nature du thread porteur.

Endpoint MetricsEndpoint

MetricsEndpoint est une ressource JAX-RS @ApplicationScoped du module dirac-rest. Trois routes (/metrics, /metrics/{scope}, /metrics/{scope}/{name}), trois étapes par appel :

  1. Résoudre le format via Accept (défaut : text/plain, OpenMetrics).

  2. Récupérer le registre cible via MetricRegistryProducerBean.registry(scope).

  3. Émettre via OpenMetricsFormatter ou JsonMetricsFormatter.

Aucun verrou n’est pris ; la sérialisation peut elle-même se paralléliser sans risque, puisque les lectures de registre sont lock-free.

Pour aller plus loin