Humboldt runs on a simple principle: no runtime reflection on the hot path, no external bytecode library, anything computable at compile time is computed there. This page describes the internal pipeline (TracerProvider → Processor → Exporter), the ScopedValue vs ThreadLocal choice, the build-time wiring policy (and its one deliberate dynamic-proxy exception), and the deliberate absence of a gRPC stack.
Signal pipeline
Each pillar is independent: one can enable traces=otlp, metrics=none, logs=in-memory without interference. Shared code (the OTLP/JSON encoders, retry, virtual-thread executor) lives in humboldt-exporter-otlp-http; the encoders sit in the non-exported io.vidocq.humboldt.exporter.otlp.http.internal package.
BatchSpanProcessor — virtual-thread worker
BatchSpanProcessor is the most delicate SDK component. Internal architecture:
-
Bounded queue (
ArrayBlockingQueue<SpanData>). Default capacity: 2048. When the queue fills, spans are dropped and aSystem.LoggerWARNING is emitted. -
Dedicated worker — a single virtual thread (
Thread.ofVirtual().name("humboldt-batch-span-processor")) looping:-
drainTo(batch, maxExportBatchSize) -
If
batch.size() >= maxExportBatchSizeOR delay elapsed OR flush requested → export. -
Calls
exporter.export(batch)which returns aCompletableResultCode.
-
-
scheduleDelay— default 5 s. Guarantees an isolated span is flushed even without saturation. -
Shutdown — final drain, awaits in-flight exports, propagates
CompletableResultCode.
Picking a virtual thread over a platform thread follows directly from Humboldt’s I/O model: the HTTP exporter blocks on the network, and with a VT this blocking does not consume a carrier thread. No pool, no platform task queue.
BatchLogRecordProcessor — same pattern
BatchLogRecordProcessor shares the pattern: VT worker (named humboldt-batch-log-processor), bounded queue, threshold + scheduleDelay + flush + shutdown. The scheduleDelay (1 s by default, shorter than traces since logs are more frequent and critical during incidents) and the dedicated queue are the only differences.
PeriodicMetricReader
Metrics follow a different model: no event queue but periodic collection. The reader calls meterProvider.collectAllMetrics() every OTEL_METRIC_EXPORT_INTERVAL ms (default 60 s for otlp, 60 min for in-memory), then hands the Collection<MetricData> to the exporter.
Context storage: ThreadLocal vs ScopedValue
OpenTelemetry delegates the current Context storage to a ContextStorageProvider SPI. Humboldt ships its own: HumboldtContextStorageProvider.
| MVP (M1, shipped) | Post-MVP (M8 ADR) | |
|---|---|---|
Implementation |
|
|
Virtual-thread pinning |
None on pure-Java code (JEP 444) |
None, with structural sharing |
Memory cost per 100 K VTs |
1 |
1 global binding, O(1) per VT |
Limitation |
Imperative |
Immutability, explicit scope ( |
The ThreadLocal MVP is OTel-spec-compliant and logs a WARNING on any out-of-order attach()/detach() — leak protection.
Build-time wiring — and one deliberate proxy exception
Humboldt follows the Vidocq philosophy: no bytecode agent, no third-party library like ASM or Byte Buddy, and no code generation of its own — Humboldt ships neither an annotation processor nor Class-File API (JEP 484) generators. What runs at build time is Vauban’s annotation processor (vauban-processor), which indexes the CDI beans of humboldt-cdi and humboldt-rest at compile time. The @WithSpan interceptor (WithSpanInterceptor) is a plain hand-written class registered through HumboldtBuildCompatibleExtension — step-debuggable, profileable like any other code.
There is one deliberate exception to the no-dynamic-proxy rule: HumboldtTelemetryProducers (in humboldt-cdi) builds the injected current-Span and current-Baggage beans with java.lang.reflect.Proxy.newProxyInstance. The MP Telemetry TCK requires an injected Span/Baggage to keep tracking the current context after injection, and a delegating proxy is the simplest spec-compliant way to do that. Everything else resolves statically.
Bootstrap sequence
-
Java Modules startup —
ServiceLoaderloadsHumboldtContextStorageProviderviaprovides … with(andMETA-INF/servicesfallback on classpath). -
HumboldtAutoConfigure.configure()is called by the applicationmain(). -
EnvConfig.system()readsSystem.getenv()+System.getProperties()(env vars win). IfOTEL_SDK_DISABLEDistrue— the MP Telemetry 2.1 default — configuration stops here: providers are built with no processor or reader, so the OTel API stays fully usable but nothing is ever exported. -
Builds the
ResourcefromOTEL_SERVICE_NAME+OTEL_RESOURCE_ATTRIBUTES. -
Based on
OTEL_TRACES_EXPORTER— instantiates the exporter (Otlp,InMemory,Logging, or no-op). -
Builds the
SamplerfromOTEL_TRACES_SAMPLER+_ARG. -
Builds the
SdkTracerProvider(Resource + Sampler + IdGenerator + Clock + appropriate processor). -
Same for
SdkMeterProvider+PeriodicMetricReader+SdkLoggerProvider+BatchLogRecordProcessor. -
Composes an
AutoConfiguredHumboldt implements OpenTelemetry, AutoCloseable. -
GlobalOpenTelemetry.set(sdk)in the application code. From there, everyTracer/Meter/Loggerresolved throughGlobalOpenTelemetry.getXxx()uses this SDK. -
On shutdown:
sdk.close()→ batch processors flush →exporter.shutdown()→httpClientreleased.
AOT compatibility
No Class.forName, Method.invoke, Class.getDeclaredFields() on the hot path. The only dynamic proxies are the two injected current-Span / current-Baggage CDI beans described above (a fixed, statically-known pair of interfaces). The remaining introspection goes through the ServiceLoader SPI, natively handled by GraalVM native-image and Leyden CDS through the standard JVM reachability metadata. Since the OTLP payload is plain JSON produced by an internal encoder, there is no protobuf runtime and no extra reflect-config.json to maintain.
Deliberate absence of gRPC
Humboldt only transports HTTP/1.1 and HTTP/2 (via java.net.http.HttpClient). No grpc-java, no netty-codec-http2. Consequence: no OTLP/gRPC transport in v1.
The future chappe-grpc module (cf. PLAN.md §3.5) will implement gRPC natively on top of the existing Chappe transport. At that point a humboldt-exporter-otlp-grpc module will ship, without grpc-java or Netty.
Internal metrology
There are no self-metrics yet: a full queue (dropped span or log record) and a failed or retried export are reported through System.Logger WARNINGs, not through dedicated humboldt.* instruments. Self-observability counters are a candidate for a later milestone.
Next: TCK.