🔎 Micrometer Tracing Cheatsheet Completo 🔎

Micrometer Tracing (parte de Spring Boot 3+ y Micrometer 1.10+) es un puente entre las aplicaciones Spring y las APIs de instrumentación de trazabilidad distribuida (principalmente OpenTelemetry). Permite instrumentar tus microservicios para recolectar información de traces y correlacionar solicitudes a través de diferentes servicios, facilitando la depuración, el monitoreo y el análisis de rendimiento en entornos distribuidos.


1. 🌟 Conceptos Clave de Trazabilidad Distribuida


2. 🛠️ Configuración Inicial (Spring Boot 3+)

  1. Añadir dependencias en pom.xml (Maven):

    • Necesitarás micrometer-tracing-bridge-otel (el puente a OpenTelemetry) y el exportador para tu sistema de trazabilidad (ej. opentelemetry-exporter-zipkin).
    <dependencies>
        <parent>...</parent> &lt;!-- Tu parent de Spring Boot -->
    
        &lt;!-- Micrometer Tracing Bridge para OpenTelemetry -->
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-tracing-bridge-otel</artifactId>
        </dependency>
    
        &lt;!-- Exporter para Zipkin (un sistema de trazabilidad popular) -->
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-exporter-zipkin</artifactId>
        </dependency>
        &lt;!-- Para la API de OpenTelemetry (opcional, si necesitas instrumentación manual más allá del bridge) -->
        &lt;!-- <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>openteelmetry-api</artifactId>
        </dependency> -->
        &lt;!-- Para pruebas -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
  2. Configurar application.yml (o application.properties):

    # src/main/resources/application.yml
    spring:
      application:
        name: my-product-service # Nombre de la aplicación (aparecerá en la traza)
      zipkin:
        base-url: http://localhost:9411 # URL de tu servidor Zipkin
      sleuth: # Propiedades legacy, aún usadas por el bridge OTel
        sampler:
          probability: 1.0 # Muestrear el 100% de las trazas (0.0 a 1.0)
          # Para producción, un valor como 0.1 (10%) o 0.01 (1%) es más común para no saturar.
        propagation:
          type: W3C # Usar el formato de encabezado W3C Trace Context (traceparent, tracestate)
          # Otros: B3 (para sistemas antiguos), B3_MULTI, B3_SINGLE
    
    # Configuración explícita del exportador OTel (opcional si spring.zipkin.base-url es suficiente)
    management:
      tracing:
        sampling:
          probability: 1.0 # Equivalente a spring.sleuth.sampler.probability
        propagation:
          type: B3 # W3C es el estándar moderno, B3 es compatible con muchos sistemas heredados
        exporters:
          zipkin:
            endpoint: http://localhost:9411/api/v2/spans # Endpoint V2 de Zipkin
          logging:
            enabled: true # Para ver las trazas en los logs de la consola (solo para desarrollo)
    • Levantar un Zipkin Server: Puedes ejecutar un servidor Zipkin localmente con Docker:
      docker run -d -p 9411:9411 openzipkin/zipkin
      • Luego, accede a la UI en http://localhost:9411.

3. 🚀 Instrumentación Automática

Micrometer Tracing instrumenta automáticamente muchos componentes de Spring Boot.

Ejemplo: Interacciones entre Microservicios

Si tienes Service A llamando a Service B (ambos instrumentados con Micrometer Tracing), verás una única traza que abarca ambas llamadas en Zipkin.

// Service A (Controller)
@RestController
@RequestMapping("/api/serviceA")
public class ServiceAController {
    private final WebClient.Builder webClientBuilder; // WebClient.Builder auto-instrumentado

    public ServiceAController(@LoadBalanced WebClient.Builder webClientBuilder) {
        this.webClientBuilder = webClientBuilder;
    }

    @GetMapping("/callServiceB")
    public Mono<String> callServiceB() {
        return webClientBuilder.build().get()
                .uri("http://service-b/api/serviceB/data") // Llama a Service B por su nombre de Eureka
                .retrieve()
                .bodyToMono(String.class);
    }
}

// Service B (Controller)
@RestController
@RequestMapping("/api/serviceB")
public class ServiceBController {
    @GetMapping("/data")
    public String getData() {
        // Esta llamada aparecerá como un span hijo de la llamada de Service A
        System.out.println("Service B received request!");
        return "Data from Service B";
    }
}

4. 📝 Instrumentación Manual

Cuando necesitas instrumentar código que no está cubierto automáticamente (ej. lógica de negocio interna, operaciones personalizadas).

4.1. Inyectar Tracer y Crear Spans

import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import org.springframework.stereotype.Service;

@Service
public class CustomBusinessService {

    private final Tracer tracer; // Inyecta el Tracer

    public CustomBusinessService(Tracer tracer) {
        this.tracer = tracer;
    }

    public String performComplexOperation(String input) {
        // 1. Iniciar un nuevo Span (o Span hijo si ya hay un contexto de Trace)
        Span span = tracer.nextSpan()
                          .name("complex-business-logic") // Nombre del Span
                          .tag("input.length", String.valueOf(input.length())) // Añadir tags (atributos)
                          .start(); // Iniciar el Span

        // Try-finally para asegurar que el span se cierra
        try (Tracer.SpanInScope ws = tracer.with='false'; // Activa el Span en el contexto de hilo actual
            // Lógica de negocio compleja aquí
            System.out.println("Executing complex operation with input: " + input);
            Thread.sleep(100); // Simular trabajo
            
            span.event("data-processed"); // Añadir un evento (log) al Span

            String result = input.toUpperCase() + "_PROCESSED";
            span.tag("output.result", result); // Añadir más tags

            return result;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            span.tag("error", "true"); // Marcar el span como error
            span.tag("error.message", e.getMessage());
            throw new RuntimeException("Operation interrupted", e);
        } finally {
            // 2. Cerrar el Span
            span.end();
        }
    }
}

4.2. Usando @NewSpan (Spring AOP - más declarativo)

import io.micrometer.tracing.annotation.NewSpan;
import org.springframework.stereotype.Service;

@Service
public class AnnotatedBusinessService {

    // Crea un nuevo Span con el nombre "my-annotated-method"
    // Los argumentos del método se pueden añadir como tags automáticamente si se configura.
    @NewSpan("my-annotated-method")
    public String executeAnnotatedOperation(String data) {
        System.out.println("Executing annotated operation: " + data);
        return data.toLowerCase();
    }
}

4.3. Baggage (Propagación de Clave-Valor)

import io.micrometer.tracing.Baggage;
import io.micrometer.tracing.BaggageManager;
import io.micrometer.tracing.Tracer;
import org.springframework.stereotype.Service;

@Service
public class BaggageService {

    private final Tracer tracer;
    private final BaggageManager baggageManager; // Inyecta BaggageManager

    public BaggageService(Tracer tracer, BaggageManager baggageManager) {
        this.tracer = tracer;
        this.baggageManager = baggageManager;
    }

    public String processWithBaggage(String userSessionId) {
        // 1. Crear un nuevo Span (o un Span hijo)
        Span span = tracer.nextSpan().name("process-user-request").start();

        try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
            // 2. Añadir Baggage al Span activo
            // Si el Baggage ya existe en la traza (propagado desde upstream), este lo sobrescribe.
            // Si no existe, lo añade.
            Baggage sessionIdBaggage = baggageManager.createBaggage("user-session-id", userSessionId);
            sessionIdBaggage.make    Current(); // Activa el baggage para el contexto actual del hilo

            System.out.println("Processing with baggage for session: " + userSessionId);

            // En un servicio downstream, se puede recuperar el baggage:
            // Baggage propagatedSessionId = Baggage.fromCurrent().get("user-session-id");
            // if (propagatedSessionId != null) {
            //     System.out.println("Propagated Session ID: " + propagatedSessionId.get());
            // }

            return "Processed for session " + userSessionId;
        } finally {
            // 3. Cerrar el Span
            span.end();
        }
    }
}

5. 🧪 Testing

Micrometer Tracing proporciona un TestSpanHandler para verificar los Spans generados en las pruebas.

import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import io.micrometer.tracing.test.simple.SimpleTracer; // Importar SimpleTracer
import io.micrometer.tracing.test.simple.SimpleSpan; // Importar SimpleSpan
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat; // AssertJ para aserciones

class CustomBusinessServiceTest {

    private SimpleTracer simpleTracer; // Implementación simple de Tracer para tests
    private CustomBusinessService service;

    @BeforeEach
    void setUp() {
        simpleTracer = new SimpleTracer();
        service = new CustomBusinessService(simpleTracer);
    }

    @Test
    void testPerformComplexOperationCreatesSpan() {
        String input = "testdata";
        service.performComplexOperation(input);

        // Obtener los spans recolectados
        assertThat(simpleTracer.get // Obtener los spans recolectados
        assertThat(simpleTracer.get=AllSpans()).hasSize(1);
        SimpleSpan span = simpleTracer.getOnlySpan();

        assertThat(span.getName()).isEqualTo("complex-business-logic");
        assertThat(span.getTag("input.length")).isEqualTo(String.valueOf(input.length()));
        assertThat(span.getEvents()).hasSize(1);
        assertThat(span.getEvents().get(0).getName()).isEqualTo("data-processed");
        assertThat(span.hasEnded()).isTrue();
    }

    @Test
    void testPerformComplexOperationHandlesError() {
        // Simular un error (ej. haciendo que la lógica interna lance una excepción)
        // Para este ejemplo, necesitaríamos modificar CustomBusinessService para simular el error
        // O mockear dependencias que lancen errores si el servicio no es puro.

        // Por simplicidad, aquí simulamos que el span se marcó como error
        String input = "error_input";
        try {
            // Suponiendo que performComplexOperation lanza una excepción para este input
            service.performComplexOperation(input);
            fail("Expected exception not thrown");
        } catch (RuntimeException e) {
            assertThat(e).hasMessageContaining("Operation interrupted");
        }

        assertThat(simpleTracer.getAllSpans()).hasSize(1);
        SimpleSpan span = simpleTracer.getOnlySpan();
        assertThat(span.getName()).isEqualTo("complex-business-logic");
        assertThat(span.isError()).isTrue(); // Verifica que el span fue marcado como error
        assertThat(span.getTag("error.message")).isNotNull();
    }
}

6. 💡 Buenas Prácticas y Consejos


Este cheatsheet te proporciona una referencia completa de Micrometer Tracing, cubriendo sus conceptos esenciales, cómo configurarlo en Spring Boot, la instrumentación automática y manual, el uso de Baggage, las pruebas y las mejores prácticas para implementar una trazabilidad distribuida efectiva en tus aplicaciones Java.