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 (@WithSpan sans value) : Class.method.

  • Tracer résolu via GlobalOpenTelemetry.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) :

  1. Lit les headers via TextMapGetter<ContainerRequestContext>.

  2. Extrait le span context via W3CPropagators.textMap().

  3. Démarre un span SERVER parent = span extrait, nom = {method} {path}.

  4. Attributs OTel : http.request.method, url.path (normalisé /), url.scheme.

  5. Stocke Span + Scope via ContainerRequestContext.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_idspan_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), ou flush()/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");
}