¿Cómo configurar el Tracing distribuido en Microservicios Java?

Disponible desde versión 5.1.0 de Onesait Platform (Survivor).

Introducción

En la release de Q2 de 2023 se ha incluido la capacidad de Tracing distribuido en Plataforma.

Esto permite obtener la trazabilidad distribuida entre sus componentes y otros componentes como microservicios que pueden invocar un API REST de plataforma y poder ver esta trazabilidad completa desde este microservicio.

Cómo acceder a la funcionalidad

Estas trazas pueden verse desde el Control Panel en la opción TRACING:

En este ejemplo se muestran las trazas de un microservicio que invoca a otro microservicio, y éste a su vez a un servicio REST expuesto desde el API Manager de Plataforma. En las trazas se ve cuánto tiempo se ha empleado en cada span, lo cual es útil para detectar cuellos de botella, interrupciones entre componentes,…

¿Cómo configurar tu microservicio para que tracee?

Instrumentación del microservicio

La instrumentación de tu microservicio permitirá que éste genere una información de trazas y span que enviará al colector. Estas trazas y span comparten un contexto por lo que puede trazarse, desde el inicio hasta el fin, el camino recorrido, el tiempo en cada punto del camino, y además contiene información del tipo clave-valor, para luego poder interpretar todo y poder sacar conclusiones.

La Plataforma utiliza Open Telemetry para crear las trazas y spans y enviarla al colector para su tratamiento.

Tipos de instrumentación

Existen dos tipos de instrumentación:

  • Está la instrumentación automática. Consiste en utilizar el agente de Open Telemetry para que de forma no intrusiva obtener las trazas sin tener que tocar el código del micro servicio, aplicación, etc., desde el que se pretendan obtener las trazas, ya que el agente se encarga de escuchar las distintas librerías más importantes que se suelen utilizar y a partir de éstas genera las trazas. Es la que usaremos por defecto.

  • Luego también está la instrumentalización manual. en este caso hay que añadir en la aplicación, micro servicio las librerías con el SDK de Open Telemetry e implementar como se van a generar las distintas trazas. Este caso es más útil cuando se pretenden generar ciertos tipos de trazas con información especifica o más customizada.

Ejemplo de Instrumentación

Para los ejemplo, crearás dos microservicios Spring Boot, uno en el que configurarás la instrumentación automática, y otro para la manual.

Instrumentación automática

Para este tipo de instrumentación tan sólo hay que arrancar junto a la aplicación el agente de Open Telemetry, el cual es altamente configurable por parametría.

En el ejemplo más sencillo tan sólo tienes que especificar dónde se encuentra el jar en los parámetros de la JVM:

-javaagent:/path/opentelemetry-javaagent.jar

Además de añadir las siguientes variables de entorno:

OTEL_JAVAAGENT_ENABLED=false #(Des)Activa el agente OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger-service:4317 #endpoint del collector OTEL_SERVICE_NAME=microservice #Nombre del servicio OTEL_METRICS_EXPORTER=none #Para que no exporte métricas, ya que no está activada esta feature

El agente puede descargarse con esta url:

download java agent. (opentelemetry-javaagent-1.27.0.jar)

También existen agentes para otras tecnologías.

Instrumentación manual

En este caso tienes que añadir código a la aplicación para generar las trazas, añadiendo estas dependencias al fichero pom.xml:

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-api</artifactId> </dependency> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-sdk</artifactId> </dependency> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-exporter-otlp</artifactId> </dependency> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-semconv</artifactId> <version>1.27.0-alpha</version> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-bom</artifactId> <version>1.27.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>

y definir cierta información en el fichero application.properties:

otel.config.trace-id-ratio-based: 1.0 otel.exporter.otlp.endpoint: https://lab.onesaitplatform.com/otelcollector/ service.name: Onesait-Platform-Microservice

En este caso, configura el ratio base, la url donde se encuentra el colector de Otel y el nombre del servicio.

Finalmente, en el microservicio, lo que se hace es invocar un servicio REST generado con el API Manager de plataforma, entonces la finalidad es crear una traza que envuelva esta llamada.

@Autowired private TracingConfiguration tracingConfiguration; private static final Logger log = LoggerFactory.getLogger(ExampleRestController.class); private Tracer tracer; @PostConstruct public void init() { tracer = tracingConfiguration.getOpenTelemetry().getTracer(ExampleRestController.class.getPackageName()); } @Autowired HttpServletRequest request; //@RequestHeader(required = false) HttpHeaders headers @GetMapping(value = "/getAll") public void getAll() throws IOException { request.getHeaderNames(); Context extractedContext = tracingConfiguration.getOpenTelemetry().getPropagators().getTextMapPropagator() .extract(Context.current(), request, Utils.getter); // the span is created try (Scope scope = extractedContext.makeCurrent()) { Span span = tracer.spanBuilder("example span with context propagation").setSpanKind(SpanKind.SERVER) .startSpan(); try (Scope ignored = span.makeCurrent()) { URL url = new URL("https://lab.onesaitplatform.com/api-manager/server/api/v1/customer/"); // from here you would be making a call to a platform rest api that queries the // information of an entity for example HttpURLConnection con = (HttpURLConnection) url.openConnection(); // adding information to the span is optional but recommended span.setAttribute(SemanticAttributes.HTTP_URL, url.toString()); span.setAttribute(SemanticAttributes.HTTP_METHOD, "GET"); span.setAttribute("component", "http"); // Inject the request with the *current* Context, which contains our current // Span. tracingConfiguration.getOpenTelemetry().getPropagators().getTextMapPropagator() .inject(Context.current(), con, Utils.setter); // the necessary information is added to the request con.setRequestProperty("accept", "application/json"); con.setRequestProperty("X-OP-APIKey", "6bfb064cb62548b78d7...."); con.setRequestMethod("GET"); int status = con.getResponseCode(); log.info("status: " + status); } finally { // the span is closed span.end(); } } }

 

En este ejemplo se obtiene la traza:

@PostConstruct public void init() { tracer = tracingConfiguration.getOpenTelemetry().getTracer(ExampleRestController.class.getPackageName()); }

Luego se obtiene el contexto por si este microservicio fuese invocado desde otro que ya hubiese creado una traza previa. Con esto, se consigue que no se parta la traza en fragmentos que partan de distintos servicios y todos parten desde una llamada inicial.

Context extractedContext = tracingConfiguration.getOpenTelemetry().getPropagators().getTextMapPropagator() .extract(Context.current(), request, Utils.getter);

Después dentro del contexto se crea un span con información especifica que el desarrollador quiere que se muestre en las trazas

try (Scope scope = extractedContext.makeCurrent()) { Span span = tracer.spanBuilder("example span with context propagation").setSpanKind(SpanKind.SERVER) .startSpan();

Al invocar al servicio REST se añade la información del contexto actual para que el próximo componente de la traza tenga el contexto previo y pueda decidir si crear una traza nueva o continuar con la actual, esto se consigue con los propagators.

tracingConfiguration.getOpenTelemetry().getPropagators().getTextMapPropagator() .inject(Context.current(), con, Utils.setter);

Para finalizar, es siempre recomendable cerrar el span.

} finally { // the span is closed span.end(); }

Si vamos a las trazas y expandimos el detalle, se comprueba que la información especifica añadida por el desarrollador es visible.

 

Para comprobar que no se pierde el contexto con esta implementación, se creó otro micro servicio Onesait-Platform-Microservice2 idéntico al mostrado pero que llamaba a Onesait-Platform-Microservice en lugar de invocar al servicio REST de plataforma, con lo que se muestra en las trazas primero el servicio Onesait-Platform-Microservice2 → Onesait-Platform-Microservice → api-manager.