Cette page rassemble les patterns courants : instrumenter une méthode métier, exposer une métrique applicative, propager du baggage à travers un appel sortant, ajuster le sampling, configurer un exporter OTLP vers un Collector. Tous les exemples supposent un runtime auto-configuré via HumboldtAutoConfigure.configure() (cf. Démarrage rapide).
Instrumenter une méthode avec @WithSpan
L’annotation standard OpenTelemetry @WithSpan (du module opentelemetry-instrumentation-annotations) est interceptée par humboldt-cdi via HumboldtBuildCompatibleExtension (CDI 4.1 Lite, compatible Vauban).
import io.opentelemetry.instrumentation.annotations.WithSpan;
import io.opentelemetry.api.trace.SpanKind;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class PaymentService {
@WithSpan(value = "payment.charge", kind = SpanKind.INTERNAL)
public Receipt charge(Order order) {
// span ouvert avant l'entrée, status ERROR + recordException si throw
return processor.process(order);
}
}
Comportement de l’interceptor WithSpanInterceptor :
-
Résolution de l’annotation : méthode prioritaire sur classe.
-
Nom par défaut (
@WithSpansansvalue) :Class.method. -
Tracerrésolu viaGlobalOpenTelemetry.get()(surchargeable pour tests). -
Sur exception :
span.recordException(ex)+span.setStatus(ERROR)puis re-throw. -
Priorité :
Interceptor.Priority.APPLICATION + 1.
Injection CDI : Tracer, Meter, Logger
Avec humboldt-cdi actif, les types OTel sont injectables en @Inject standard. Producers fournis par HumboldtTelemetryProducers :
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.api.metrics.LongCounter;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class CheckoutMetrics {
private final LongCounter checkouts;
@Inject
public CheckoutMetrics(Meter meter) {
this.checkouts = meter
.counterBuilder("checkout.total")
.setDescription("Nombre de checkouts traités")
.setUnit("1")
.build();
}
public void recordSuccess() {
checkouts.add(1);
}
}
Instrumentation manuelle d’un span enfant
Quand @WithSpan n’est pas adapté (granularité fine, span dynamique) :
Tracer tracer = GlobalOpenTelemetry.getTracer("mon.application");
Span parent = Span.current();
Span child = tracer.spanBuilder("db.query")
.setSpanKind(SpanKind.CLIENT)
.setAttribute("db.system", "postgresql")
.setAttribute("db.statement", "SELECT * FROM orders WHERE id = ?")
.startSpan();
try (Scope ignored = child.makeCurrent()) {
return executeQuery();
} catch (SQLException ex) {
child.recordException(ex);
child.setStatus(StatusCode.ERROR, ex.getMessage());
throw ex;
} finally {
child.end();
}
Propagation HTTP automatique (entrée)
Avec humboldt-rest actif, tout endpoint JAX-RS est automatiquement instrumenté. Comportement de HumboldtServerRequestFilter (@Provider) :
-
Lit les headers via
TextMapGetter<ContainerRequestContext>. -
Extrait le span context via
W3CPropagators.textMap(). -
Démarre un span
SERVERparent = span extrait, nom ={method} {path}. -
Attributs OTel :
http.request.method,url.path(normalisé/),url.scheme. -
Stocke
Span+ScopeviaContainerRequestContext.setProperty().
HumboldtServerResponseFilter ferme le span : http.response.status_code, status ERROR si ≥ 500, Scope.close() puis span.end() en finally.
Aucun code applicatif n’est requis : il suffit d’avoir humboldt-rest au classpath et Cassini comme runtime JAX-RS.
Propagation HTTP manuelle (sortie)
Pour un appel HTTP sortant avec un client autre que cassini-client :
HttpRequest.Builder builder = HttpRequest.newBuilder(uri);
GlobalOpenTelemetry.getPropagators()
.getTextMapPropagator()
.inject(Context.current(), builder, (b, key, value) -> b.header(key, value));
HttpRequest request = builder.build();
HttpResponse<String> resp = client.send(request, BodyHandlers.ofString());
L'`inject()` ajoute traceparent, tracestate et baggage aux headers.
Baggage — valeurs applicatives propagées
Le Baggage transporte des paires clé/valeur (utilisateur, tenant, feature flag) à travers les frontières de service.
import io.opentelemetry.api.baggage.Baggage;
Baggage baggage = Baggage.builder()
.put("tenant.id", "acme")
.put("feature.new_checkout", "true")
.build();
try (Scope ignored = baggage.makeCurrent()) {
callService(); // headers HTTP sortants porteront baggage: tenant.id=acme,...
}
Lecture côté serveur : Baggage.current().getEntryValue("tenant.id").
Métriques applicatives
Trois familles d’instruments OTel sont supportées par humboldt-sdk-metric :
Meter meter = GlobalOpenTelemetry.getMeter("mon.application");
// Counter — monotone, somme
LongCounter requests = meter.counterBuilder("http.server.requests")
.setUnit("1").build();
// Histogram — distribution (latences, tailles)
DoubleHistogram latency = meter.histogramBuilder("http.server.duration")
.setUnit("ms").build();
// Gauge async — valeur lue à chaque collecte
meter.gaugeBuilder("queue.size")
.ofLongs()
.buildWithCallback(measurement -> measurement.record(queue.size()));
requests.add(1, Attributes.of(
AttributeKey.stringKey("http.method"), "GET",
AttributeKey.longKey("http.status_code"), 200L));
latency.record(durationMs);
PeriodicMetricReader collecte toutes les 60 s (configurable via OTEL_METRIC_EXPORT_INTERVAL).
Logs OTel
humboldt-sdk-log expose SdkLoggerProvider. Les bridges JUL et SLF4J sont en M5b (différés). En attendant, émission directe possible :
io.opentelemetry.api.logs.Logger logger =
GlobalOpenTelemetry.getLogsBridge().get("mon.application");
logger.logRecordBuilder()
.setSeverity(Severity.INFO)
.setSeverityText("INFO")
.setBody("checkout completed")
.setAttribute(AttributeKey.stringKey("order.id"), order.id())
.emit();
LogRecordBuilder capture automatiquement le Span courant (corrélation trace_id ↔ span_id).
Sampling — ajuster le débit
Les samplers se configurent via env vars :
# Tout tracer (dev)
export OTEL_TRACES_SAMPLER=always_on
# Échantillon déterministe 5 % (prod typique)
export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.05
# Couper toute trace (incident perf, debug d'isolation)
export OTEL_TRACES_SAMPLER=always_off
ParentBased est recommandé en prod : on suit la décision du service amont si présent (cohérence par trace), sinon on applique le ratio.
Exporter OTLP — endpoint et headers
# Endpoint commun (suffixe /v1/traces, /v1/metrics, /v1/logs ajouté)
export OTEL_EXPORTER_OTLP_ENDPOINT=http://collector.observability:4318
# Override par signal
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://traces.example/v1/traces
# Auth (clé API, basic, etc.)
export OTEL_EXPORTER_OTLP_HEADERS="authorization=Bearer abcdef123,x-tenant=acme"
Le humboldt-exporter-otlp-http exporter utilise java.net.http.HttpClient avec executor virtual threads, retry exponentiel borné (100ms × 2^attempt, plafond 5 s) sur 5xx et erreurs réseau, max 5 tentatives.
Batching et flush
BatchSpanProcessor (worker virtual thread) accumule les spans et les envoie par lots :
-
Queue bornée — drop des spans en cas de saturation (compté via metric interne).
-
Trigger : seuil de batch atteint, délai schedulé écoulé (
scheduleDelay), ouflush()/shutdown()explicite.
// Flush synchrone avant shutdown (ex: shutdown hook applicatif)
sdk.flush().join(10, TimeUnit.SECONDS);
Tests : exporter in-memory
Pour les tests unitaires sans Collector :
export OTEL_TRACES_EXPORTER=in-memory
export OTEL_METRICS_EXPORTER=in-memory
export OTEL_LOGS_EXPORTER=in-memory
try (AutoConfiguredHumboldt sdk = HumboldtAutoConfigure.configure()) {
GlobalOpenTelemetry.set(sdk);
runCodeUnderTest();
sdk.flush().join(5, TimeUnit.SECONDS);
List<SpanData> spans = sdk.inMemorySpanExporter().getFinishedSpanItems();
assertThat(spans).extracting(SpanData::getName).contains("payment.charge");
}
Suivant : Fonctionnement interne.