Humboldt fonctionne sur un principe simple : aucune réflexion runtime sur le hot path, aucune librairie de bytecode externe, tout ce qui peut être calculé à la compilation l’est. Cette page décrit le pipeline interne (TracerProviderProcessorExporter), le choix ScopedValue vs ThreadLocal, la génération de code par APT et Class-File API, et l’absence volontaire de stack gRPC.

Pipeline des signaux

Diagram

Chaque pilier est indépendant : on peut activer traces=otlp, metrics=none, logs=in-memory sans interférence. Le code partagé (encoder JSON/protobuf, retry, executor virtual threads) est dans humboldt-exporter-otlp-http.

BatchSpanProcessor — worker virtual thread

Le BatchSpanProcessor est le composant le plus délicat du SDK. Architecture interne :

  • Queue bornée (ArrayBlockingQueue<SpanData>). Capacité par défaut : 2048. Lorsque la queue est pleine, les spans sont droppés et un compteur interne incrémenté.

  • Worker dédié — un seul virtual thread (Thread.ofVirtual().name("humboldt-batch-worker")) qui boucle :

    1. drainTo(batch, maxExportBatchSize)

    2. Si batch.size() >= maxExportBatchSize OU délai dépassé OU flush demandé → export.

    3. Appelle exporter.export(batch) qui retourne un CompletableResultCode.

  • scheduleDelay — défaut 5 s. Garantit qu’un span isolé sera flushé même sans saturation.

  • Shutdown — drain final, attend les exports en vol, propage CompletableResultCode.

Le choix d’un virtual thread plutôt qu’un platform thread découle directement du modèle d’I/O d’Humboldt : l’exporter HTTP bloque sur le réseau, et avec un VT ce blocage ne consomme pas de carrier thread. Aucun pool, aucune file de tâches plateforme.

BatchLogRecordProcessor — pattern aligné

Le BatchLogRecordProcessor partage le pattern : worker VT, queue bornée, threshold + scheduleDelay + flush + shutdown. Les paramètres scheduleDelay (1 s par défaut, plus court que les traces car les logs sont plus fréquents et critiques en cas d’incident) et la file dédiée sont les seules différences.

PeriodicMetricReader

Pour les métriques, le modèle est différent : pas de queue d’évènements mais une collecte périodique. Le reader appelle meterProvider.collectAllMetrics() toutes les OTEL_METRIC_EXPORT_INTERVAL ms (défaut 60 s pour otlp, 60 min pour in-memory), puis transmet la Collection<MetricData> à l’exporter.

Stockage du Context : ThreadLocal vs ScopedValue

OpenTelemetry délègue le stockage du Context courant à un ContextStorageProvider SPI. Humboldt fournit le sien : HumboldtContextStorageProvider.

MVP (M1, livré) Post-MVP (M8 ADR)

Implémentation

ThreadLocal<Context>

ScopedValue<Context> (JEP 506)

Pinning virtual threads

Aucun sur le code Java pur (JEP 444)

Aucun, et avec structural sharing

Coût mémoire pour 100 K VTs

1 entrée ThreadLocal par VT

1 binding global, O(1) par VT

Limitation

Mutations par attach()/detach() impératives

Immutabilité, scope explicite (runWhere(…​))

Le MVP ThreadLocal est conforme contrat OTel et log un WARNING sur tout attach()/detach() désordonné — protection contre les fuites de scope.

Codegen APT + Class-File API

Humboldt suit la philosophie Vidocq : aucun bytecode agent, aucun proxy dynamique, aucune librairie tierce comme ASM ou Byte Buddy. La génération de code se fait à process-classes via :

  • APT (javax.annotation.processing) — découverte des annotations @WithSpan (et futures), génération de méta-données dans META-INF/, vérification statique (typage attributs, kind valide).

  • Class-File API (JEP 484, finalisée Java 25) — émission directe des classes générées (interceptors, providers, bridges) sans librairie externe.

Cela garantit :

  1. AOT-ready — GraalVM native-image et Leyden CDS ne nécessitent aucun reachability metadata pour Humboldt ; tout est statique.

  2. JPMS clean — les classes générées sont émises dans les bons modules avec les bons accès.

  3. Pas de proxy dynamique — l’interceptor @WithSpan est une classe statique normale, debuggable au pas à pas, profilable comme du code écrit à la main.

Séquence de bootstrap

  1. JPMS startupServiceLoader charge HumboldtContextStorageProvider via provides …​ with (et fallback META-INF/services en classpath).

  2. HumboldtAutoConfigure.configure() est appelé par le main() applicatif.

  3. EnvConfig.fromProcess() lit System.getenv() + System.getProperties().

  4. Construction du Resource à partir de OTEL_SERVICE_NAME + OTEL_RESOURCE_ATTRIBUTES.

  5. Selon OTEL_TRACES_EXPORTER — instancie l’exporter (Otlp, InMemory, Logging, ou no-op).

  6. Construction du Sampler selon OTEL_TRACES_SAMPLER + _ARG.

  7. Construction du SdkTracerProvider (Resource + Sampler + IdGenerator + Clock + processor adapté).

  8. Idem pour SdkMeterProvider + PeriodicMetricReader + SdkLoggerProvider + BatchLogRecordProcessor.

  9. Composition d’un AutoConfiguredHumboldt implements OpenTelemetry, AutoCloseable.

  10. GlobalOpenTelemetry.set(sdk) dans le code applicatif. À partir d’ici, tout Tracer/Meter/Logger résolu via GlobalOpenTelemetry.getXxx() utilise ce SDK.

  11. À l’arrêt : sdk.close() → flush des processors batch → exporter.shutdown()httpClient détaché.

Compatibilité AOT

Aucun appel Class.forName, Method.invoke, Class.getDeclaredFields(), Proxy.newProxyInstance() sur le hot path. La seule introspection passe par le SPI ServiceLoader, gérée nativement par GraalVM native-image et Leyden CDS via les reachability metadata standard de la JVM.

Note sur protobuf : protobuf-java (3.x) est compatible GraalVM moderne avec quelques reflect-config.json pour les DescriptorProto ; Humboldt fournit ces fichiers dans META-INF/native-image/ (jalon M8).

Absence volontaire de gRPC

Humboldt ne transporte que du HTTP/1.1 et HTTP/2 (via java.net.http.HttpClient). Pas de grpc-java, pas de netty-codec-http2. Conséquence : pas de transport OTLP/gRPC en v1.

Le futur module chappe-grpc (cf. PLAN.md §3.5) implémentera gRPC nativement sur le transport Chappe existant. À ce moment-là, un module humboldt-exporter-otlp-grpc sera livré, sans grpc-java ni Netty.

Métrologie interne

Le BatchSpanProcessor expose des compteurs internes :

  • humboldt.exporter.spans.dropped — spans droppés sur queue pleine.

  • humboldt.exporter.batch.size — distribution des tailles de batch effectives.

  • humboldt.exporter.export.duration — latence HTTP par signal.

  • humboldt.exporter.retry.count — nombre de retries effectués.

Ces métriques sont émises via le SdkMeterProvider interne — observabilité de l’observabilité.

Suivant : TCK.