Comprensión de APM: cómo agregar extensiones al agente Java de OpenTelemetry

por David Esperanza

Sin acceso al código, las SRE y las operaciones de TI no siempre pueden obtener la visibilidad que necesitan

Como SRE, ¿alguna vez ha estado en una situación en la que está desarrollando una aplicación escrita utilizando un marco no estándar, o desea obtener algunos datos comerciales interesantes de su aplicación (como la cantidad de pedidos procesados), pero usted no tiene Do? ¿Tienes acceso al código fuente?

Todos sabemos que este puede ser un escenario desafiante, que genera brechas en la visibilidad, la incapacidad de rastrear completamente el código de un extremo a otro y la pérdida de datos de monitoreo críticos para el negocio que ayudan a comprender el verdadero impacto de un problema.

¿Cómo podemos solucionar este problema? Discutimos un enfoque en los siguientes tres blogs:

Estamos aquí para desarrollar un complemento para el agente APM de Elastic® para ayudar a acceder a datos comerciales clave para monitorear y agregar seguimientos donde no existe nada.
  
En este blog, discutiremos cómo hacer lo mismo con el agente Java de OpenTelemetry usando el marco de extensión.

Conceptos básicos: cómo funciona APM

Antes de continuar, primero comprendamos algunos conceptos básicos y terminología.

  • Agente Java : esta es una herramienta que se puede utilizar para inspeccionar (o modificar) el código de bytes de los archivos de clase en la máquina virtual Java (JVM). Los agentes de Java se pueden usar para una variedad de propósitos, como monitoreo de rendimiento, registro, seguridad y más.
  • Bytecode : este es el código intermedio generado por el compilador de Java a partir del código fuente de Java. Este código es interpretado o compilado sobre la marcha por la JVM para producir un código de máquina ejecutable.
  • Byte Buddy : Byte Buddy es una biblioteca de manipulación y generación de código Java. Se utiliza para crear, modificar o ajustar clases de Java en tiempo de ejecución. En el contexto de los agentes de Java, Byte Buddy proporciona una forma potente y flexible de modificar el código de bytes. Tanto el agente de Elastic APM como el agente de OpenTelemetry utilizan Byte Buddy en segundo plano .

Ahora, hablemos de cómo funciona la detección automática con Byte Buddy :

La autoinstrumentación es el proceso mediante el cual un agente modifica el código de bytes de una clase de aplicación, generalmente para insertar código de monitoreo. El agente no modifica directamente el código fuente, sino que modifica el código de bytes cargado en la JVM. Esto se hace cuando la JVM carga la clase, por lo que la modificación es válida en tiempo de ejecución.

Aquí hay una descripción simplificada del proceso:

1) Inicie la JVM con un agente : al iniciar una aplicación Java, puede utilizar la opción de línea de comandos -javaagent para especificar el agente Java. Esto le indica a la JVM que cargue el proxy antes de llamar al método principal de la aplicación. En este punto, el agente tiene la oportunidad de configurar el transformador de clase.

2) Registrar el transformador de archivos con Byte Buddy : Su agente registrará el transformador de archivos con Byte Buddy. Un convertidor es una pieza de código que se llama cada vez que se carga una clase en la JVM. Este convertidor recibe el código de bytes de una clase y puede modificar ese código de bytes antes de usar la clase.

3) Convertir código de bytes : cuando se llama al convertidor, utilizará la API de Byte Buddy para modificar el código de bytes. Byte Buddy le permite especificar transformaciones de una manera expresiva y de alto nivel, en lugar de escribir códigos de bytes complejos a mano. Por ejemplo, puede especificar una determinada clase y método dentro de esa clase para instrumentar y proporcionar un "interceptor" para agregar un nuevo comportamiento a ese método.

  • Por ejemplo, suponga que desea medir el tiempo de ejecución de un método. Le indicará a Byte Buddy que apunte a clases y métodos específicos, luego proporcione un interceptor que envuelva la llamada al método con código de tiempo. Cada vez que se llama a este método, primero llamará a su interceptor, medirá la hora de inicio, luego llamará al método original y finalmente medirá la hora de finalización e imprimirá la duración.

4) Usar clases transformadas : una vez que el proxy ha configurado sus transformadores, la JVM continúa cargando clases como de costumbre. Los transformadores se llaman cada vez que se carga una clase, lo que les permite modificar el código de bytes. Luego, su aplicación utilizará estas clases transformadas como si fueran las clases originales, pero ahora tienen el comportamiento adicional que inyectó a través del interceptor.

 Esencialmente, la instrumentación automática de Byte Buddy modifica el comportamiento de las clases de Java en tiempo de ejecución sin requerir cambios directos en el código fuente. Esto es especialmente útil para cuestiones transversales como el registro, la supervisión o la seguridad, ya que le permite centralizar este código en el agente de Java en lugar de distribuirlo por toda la aplicación.

Aplicaciones, requisitos previos y configuración

Hay una aplicación muy simple en este repositorio de GitHub que se usa en todo el blog. Todo lo que hace es pedirle que ingrese un texto y luego cuente las palabras.

También se enumera a continuación:

package org.davidgeorgehope;
import java.util.Scanner;
import java.util.logging.Logger;

public class Main {
    private static Logger logger = Logger.getLogger(Main.class.getName());

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.println("Please enter your sentence:");
            String input = scanner.nextLine();
            Main main = new Main();
            int wordCount = main.countWords(input);
            System.out.println("The input contains " + wordCount + " word(s).");
        }
    }
    public int countWords(String input) {

        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        if (input == null || input.isEmpty()) {
            return 0;
        }

        String[] words = input.split("\\s+");
        return words.length;
    }
}

Para los propósitos de este blog, usaremos Elastic Cloud para capturar datos generados por OpenTelemetry; siga las instrucciones aquí para comenzar con Elastic Cloud .

Una vez que comience a usar Elastic Cloud, obtenga la configuración de OpenTelemetry desde la página de APM:

Necesitarás esto más tarde. Si desea implementar en su propia computadora, puede consultar el artículo " Elástico: una guía para desarrolladores " para implementar Elasticsearch y Kibana.

Finalmente, descargue OpenTelemetry Agent .

Inicie la aplicación y OpenTelemetry

Si está comenzando con esta sencilla aplicación, constrúyala y ejecútela como lo haría con el Agente de OpenTelemetry, completando los valores apropiados con las variables obtenidas anteriormente.

java -javaagent:opentelemetry-javaagent.jar -Dotel.exporter.otlp.endpoint=XX -Dotel.exporter.otlp.headers=XX -Dotel.metrics.exporter=otlp -Dotel.logs.exporter=otlp -Dotel.resource.attributes=XX -Dotel.service.name=your-service-name -jar simple-java-1.0-SNAPSHOT.jar

Verás que no pasó nada. La razón es que OpenTelemetry Agent no tiene forma de saber qué monitorear. La forma en que funciona APM con detección automática es que " conoce " los marcos estándar (como Spring o HTTPClient) y puede ganar visibilidad " inyectando " automáticamente el código de seguimiento en estos marcos estándar.

No conoce org.davidgeorgehope.Main en nuestra sencilla aplicación Java.

Afortunadamente, podemos agregar esta funcionalidad usando el marco OpenTelemetry Extensions .

Extensiones de OpenTelemetry

En el repositorio anterior, además de la aplicación simple-java, hay un complemento Elastic APM y una extensión OpenTelemetry. Los archivos relevantes para OpenTelemetry Extension se encuentran aquí : WordCountInstrumentation.java y WordCountInstrumentationModule.java.

Notará que tanto OpenTelemetry Extensions como Elastic APM Plugins usan Byte Buddy, una biblioteca común para la instrumentación de código. Sin embargo, existen algunas diferencias clave en la forma en que se guía el código.

La clase WordCountInstrumentationModule amplía la clase InstrumentationModule específica de OpenTeletry, cuyo propósito es describir un conjunto de TypeInstrumentations que deben aplicarse juntas para instrumentar correctamente una biblioteca en particular. La clase WordCountInstrumentation es una de esas instancias de TypeInstrumentation.

La detección de tipos está centralizada en un módulo para clases auxiliares compartidas, verificaciones de tiempo de ejecución de entrada y criterios aplicables del cargador de clases, y solo se puede habilitar o deshabilitar como grupo.

Esto es un poco diferente de cómo funciona el complemento Elastic APM, porque el método predeterminado para inyectar código con OpenTelemetry está en línea con OpenTelemetry (que es el método predeterminado), y puede usar la configuración de InstrumentationModule para inyectar dependencias en el cargador de clases de aplicaciones principales ( Como se muestra abajo). El enfoque de Elastic APM, que es más seguro porque permite el aislamiento de clases auxiliares y es más fácil de depurar con un IDE normal, se contribuyó a OpenTelemetry. Aquí inyectamos la clase TypeInstrumentation y la clase WordCountInstrumentation en el cargador de clases.

 @Override
    public List<String> getAdditionalHelperClassNames() {
        return List.of(WordCountInstrumentation.class.getName(),"io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation");
    }

Otra parte interesante de la clase TypeInstrumentation es la configuración.

Aquí damos un nombre a nuestro "grupo" de detección. Un InstrumentationModule debe tener al menos un nombre. Los usuarios de javaagent pueden suprimir las detecciones seleccionadas haciendo referencia a uno de sus nombres. Los nombres de los módulos de detección se nombran con guiones.

    public WordCountInstrumentationModule() {
        super("wordcount-demo", "wordcount");
    }

Entre otras cosas, vemos que los métodos de esta clase pueden especificar opcionalmente un orden de carga en relación con otra instrumentación, y especificamos una clase que amplía TypeInstrumentation y es responsable de la mayor parte de la instrumentación.

Veamos la clase WordCountInstrumention, que ahora amplía TypeInstrumention:

// The WordCountInstrumentation class implements the TypeInstrumentation interface.
// This allows us to specify which types of classes (based on some matching criteria) will have their methods instrumented.

public class WordCountInstrumentation implements TypeInstrumentation {

    // The typeMatcher method is used to define which classes the instrumentation should apply to.
    // In this case, it's the "org.davidgeorgehope.Main" class.
    @Override
    public ElementMatcher<TypeDescription> typeMatcher() {
        logger.info("TEST typeMatcher");
        return ElementMatchers.named("org.davidgeorgehope.Main");
    }

    // In the transform method, we specify which methods of the classes matched above will be instrumented, 
    // and also the advice (a piece of code) that will be added to these methods.
    @Override
    public void transform(TypeTransformer typeTransformer) {
        logger.info("TEST transform");
        typeTransformer.applyAdviceToMethod(namedOneOf("countWords"),this.getClass().getName() + "$WordCountAdvice");
    }

    // The WordCountAdvice class contains the actual pieces of code (advices) that will be added to the instrumented methods.
    @SuppressWarnings("unused")
    public static class WordCountAdvice {
        // This advice is added at the beginning of the instrumented method (OnMethodEnter).
        // It creates and starts a new span, and makes it active.
        @Advice.OnMethodEnter(suppress = Throwable.class)
        public static Scope onEnter(@Advice.Argument(value = 0) String input, @Advice.Local("otelSpan") Span span) {
            // Get a Tracer instance from OpenTelemetry.
            Tracer tracer = GlobalOpenTelemetry.getTracer("instrumentation-library-name","semver:1.0.0");
            System.out.print("Entering method");

            // Start a new span with the name "mySpan".
            span = tracer.spanBuilder("mySpan").startSpan();

            // Make this new span the current active span.
            Scope scope = span.makeCurrent();

            // Return the Scope instance. This will be used in the exit advice to end the span's scope.
            return scope; 
        }

        // This advice is added at the end of the instrumented method (OnMethodExit).
        // It first closes the span's scope, then checks if any exception was thrown during the method's execution.
        // If an exception was thrown, it sets the span's status to ERROR and ends the span.
        // If no exception was thrown, it sets a custom attribute "wordCount" on the span, and ends the span.
        @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
        public static void onExit(@Advice.Return(readOnly = false) int wordCount,
                                  @Advice.Thrown Throwable throwable,
                                  @Advice.Local("otelSpan") Span span,
                                  @Advice.Enter Scope scope) {
            // Close the scope to end it.
            scope.close();

            // If an exception was thrown during the method's execution, set the span's status to ERROR.
            if (throwable != null) {
                span.setStatus(StatusCode.ERROR, "Exception thrown in method");
            } else {
                // If no exception was thrown, set a custom attribute "wordCount" on the span.
                span.setAttribute("wordCount", wordCount);
            }

            // End the span. This makes it ready to be exported to the configured exporter (e.g. Elastic).
            span.end();
        }
    }
}

La clase objetivo que detectamos se define en el método typeMatch y el método que queremos detectar se define en el método Transform. Nuestro objetivo es la clase Main y el método countWords.

Como puede ver, aquí tenemos una clase interna que hace la mayor parte del trabajo de definir los métodos onEnter y onExit, que nos dicen qué hacer cuando ingresamos al método countWords y cuando salimos del método countWords.

En el método onEnter, configuramos un nuevo intervalo de OpenTelemetry y en el método onExit, finalizamos el intervalo. Si el método finaliza con éxito, también obtenemos el recuento de palabras y lo agregamos a la propiedad.

Ahora veamos qué sucede cuando lo ejecutamos. La buena noticia es que lo hemos hecho realmente fácil al proporcionar un único archivo docker para que usted haga todo el trabajo.

ponlo todo junto

Clone el repositorio de GitHub si aún no lo ha hecho y, antes de continuar, echemos un vistazo rápido al archivo docker que estamos usando.

# Build stage
FROM maven:3.8.7-openjdk-18 as build

COPY simple-java /home/app/simple-java
COPY opentelemetry-custom-instrumentation /home/app/opentelemetry-custom-instrumentation

WORKDIR /home/app/simple-java
RUN mvn install

WORKDIR /home/app/opentelemetry-custom-instrumentation
RUN mvn install

# Package stage
FROM maven:3.8.7-openjdk-18
COPY --from=build /home/app/simple-java/target/simple-java-1.0-SNAPSHOT.jar /usr/local/lib/simple-java-1.0-SNAPSHOT.jar
COPY --from=build /home/app/opentelemetry-custom-instrumentation/target/opentelemetry-custom-instrumentation-1.0-SNAPSHOT.jar /usr/local/lib/opentelemetry-custom-instrumentation-1.0-SNAPSHOT.jar

WORKDIR /

RUN curl -L -o opentelemetry-javaagent.jar https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar

COPY start.sh /start.sh
RUN chmod +x /start.sh

ENTRYPOINT ["/start.sh"]

El dockerfile se divide en dos partes: durante el proceso de compilación de la ventana acoplable, creamos la aplicación Java simple desde el origen y luego creamos la instrumentación personalizada. Después de eso, descargamos el último agente Java de OpenTelemetry. En tiempo de ejecución, simplemente ejecutamos el archivo start.sh como se describe a continuación:

#!/bin/sh
java \
-javaagent:/opentelemetry-javaagent.jar \
-Dotel.exporter.otlp.endpoint=${SERVER_URL} \
-Dotel.exporter.otlp.headers="Authorization=Bearer ${SECRET_KEY}" \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-Dotel.resource.attributes=service.name=simple-java,service.version=1.0,deployment.environment=production \
-Dotel.service.name=your-service-name \
-Dotel.javaagent.extensions=/usr/local/lib/opentelemetry-custom-instrumentation-1.0-SNAPSHOT.jar \
-Dotel.javaagent.debug=true \
-jar /usr/local/lib/simple-java-1.0-SNAPSHOT.jar

Hay dos cosas importantes a tener en cuenta sobre este script: la primera es que iniciamos el conjunto de parámetros javaagent en opentelemetry-javaagent.jar; esto inicia la ejecución de OpenTelemetry javaagent, que comienza antes de que se ejecute cualquier código.

En este contenedor, debe haber una clase con un método principal que buscará la JVM. Esto iniciará el agente de Java. Como se mencionó anteriormente, cualquier código de bytes compilado se filtra esencialmente a través del código javaagent, por lo que puede modificar las clases antes de ejecutarse.

La segunda cosa importante aquí es la configuración de javaagent.extensions, que carga las extensiones que construimos para instrumentar nuestra aplicación simple-java.

Ahora ejecuta el siguiente comando:

docker build -t djhope99/custom-otel-instrumentation:1 .
docker run -it -e 'SERVER_URL=XXX' -e 'SECRET_KEY=XX djhope99/custom-otel-instrumentation:1

Si usa SERVER_URL y SECRET_KEY obtenidos anteriormente aquí, debería ver esta conexión con Elastic.

Cuando se inicie, le pedirá que escriba una oración, escriba algunas oraciones y presione enter. Haga esto varias veces: aquí hay un sueño para forzar una transacción de larga duración:

 

Eventualmente, verá el servicio que se muestra en el mapa de servicios:

Aparecerá un rastro:

 

Dentro de ese lapso, verá el atributo de recuento de palabras que recopilamos:

Esto se puede usar para más tableros y AI/ML, incluida la detección de anomalías (si es necesario), que es fácil de hacer como se muestra a continuación.

Primero haga clic en el ícono de Elastic a la izquierda y seleccione Tablero para crear un nuevo tablero:

Desde aquí, haga clic en Crear visualización.

 

Busque la etiqueta de recuento de palabras en el índice de APM, como se muestra a continuación:

Como puede ver, dado que creamos esta propiedad en el código Span como se muestra a continuación, con wordCount como un tipo "Integer", pudimos asignarlo automáticamente como un campo numérico en Elastic: 

span.setAttribute("wordCount", wordCount);

¡Desde aquí podemos arrastrarlo y soltarlo en la visualización para mostrarlo en nuestro tablero! muy facil.

En resumen

Este blog arroja luz sobre el papel invaluable de los agentes Java de OpenTelemetry para cerrar las brechas de visibilidad y obtener datos de monitoreo críticos para el negocio, especialmente cuando el acceso al código fuente no está disponible.

Este blog cubre una comprensión básica de Java Agent, Bytecode y Byte Buddy, seguido de un examen exhaustivo del proceso de detección automática de Byte Buddy.

La implementación del agente Java de OpenTelemetry utilizando el marco de extensión se demuestra con la ayuda de una aplicación Java simple, que destaca la capacidad del agente para inyectar código de seguimiento en la aplicación con fines de supervisión.

Detalla cómo configurar el agente e integrar OpenTelemetry Extension, y describe el funcionamiento de una aplicación de muestra para ayudar a los usuarios a comprender la aplicación práctica de la información discutida. Esta esclarecedora publicación de blog es un excelente recurso para SRE y operaciones de TI que buscan utilizar las capacidades de detección automática de OpenTelemetry para optimizar sus esfuerzos de aplicación.

原文:Comprender APM: cómo agregar extensiones al agente Java de OpenTelemetry | Blog elástico

Supongo que te gusta

Origin blog.csdn.net/UbuntuTouch/article/details/131931799
Recomendado
Clasificación