Mostrando entradas con la etiqueta monitorizacion. Mostrar todas las entradas
Mostrando entradas con la etiqueta monitorizacion. Mostrar todas las entradas

sábado, 27 de noviembre de 2021

Spring Boot Admin: Iniciación

Hoy vamos a ver una genial aplicación que nos permitirá monitorizar nuestras aplicaciones de Spring Boot, Spring Boot Admin a.k.a SBA.  Esta aplicación tiene múltiples cualidades pero para poder hacer uso de ellas, primero tenemos que configurarlo.  Y en este post nos centraremos en como configurarlo principalmente, primero con el método por defecto y luego haciendo uso de Eureka. Para ello, hemos utilizado las siguientes versiones:

  • Spring-boot: 2.5.7
  • Spring-admin: 2.5.4
  • Spring-cloud: 2020.0.3

Para empezar crearemos un servidor que será el encargado de recopilar la información y proporcionarnos una interfaz gráfica para acceder a los mismos. 

Vamos a empezar con una versión básica. Crearemos un servidor que se pueda registrar a si mismo. Para ello deberemos seguir los siguientes pasos:

  • Añadir la dependencia de spring spring-boot-starter-web. 
  • Añadir la dependencia de codecentric spring-boot-admin-starter-server que contendrá el motor del servidor.
  • Añadir la dependencia de codecentric spring-boot-admin-starter-client que nos permitirá conectar el propio servidor consigo mismo. 
  • Crear la clase de arranque y configuración con las anotaciones: @EnableAdminServer, @EnableAutoConfiguration, @SpringBootApplication.
  • Indicar a través de la propiedad spring.boot.admin.client.url en el fichero de configuración la ruta donde estará nuestra aplicación. Para el ejemplo: http://localhost:8080.
  • Indicar a través de la propiedad management.endpoints.web.exposure.include la habilitación de los endpoints de Actuator. 
Ya solo tenemos que arrancar la aplicación y podemos verla desplegada y auto descubierta. 


Con esto tendremos la configuración básica, pero tenemos que tener en cuenta que es una aplicación para la administración de otras aplicaciones. Y aunque en ciertos casos puede desplegarse dentro de una red interna donde no tendremos problemas de seguridad, también es posible que sea desplegado en entornos productivos y por tanto debe de tener algún tipo de seguridad, sobre todo si esta disponible desde Internet. Para ello añadiremos una configuración básica que nos permita incluir una pantalla de logín y un pequeño filtro de usuario. Y para llevarlo a cabo debemos realizar los siguientes pasos:
  • Añadimos la librería spring-boot-starter-security
  • En el fichero de configuración, añadimos las propiedades spring.security.user.name/passsword que configuren un usuario por defecto. 
  • En el fichero de configuración, añadimos las propiedades spring.boot.admin.client.username/password que permitan al cliente, el mismo, registrarse en el servidor
  • En el fichero de configuración, añadimos las propiedades spring.boot.admin.client.instance.metadata.user.name/password que permitan al servidor acceder a los endpoints securizados de métrica del cliente.
  • Además añadimos una clase java de configuración de seguridad que permita acceder sin restricciones a los endpoints info y health además de a la pantalla de login. Securizando el resto de URLs y endpoints de la aplicación. Configuración obtenida de la documentación oficial, aquí.
@Configuration(proxyBeanMethods = false)
public class SecuritySecureConfig extends WebSecurityConfigurerAdapter {
  private final AdminServerProperties adminServer;
  private final SecurityProperties security;

  public SecuritySecureConfig(AdminServerProperties adminServer, SecurityProperties security) {
    this.adminServer = adminServer;
    this.security = security;
  }
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
    successHandler.setTargetUrlParameter("redirectTo");
    successHandler.setDefaultTargetUrl(this.adminServer.path("/"));

    http.authorizeRequests(
        (authorizeRequests) -> authorizeRequests.antMatchers(this.adminServer.path("/assets/**")).permitAll() 
            .antMatchers(this.adminServer.path("/actuator/info")).permitAll()
            .antMatchers(this.adminServer.path("/actuator/health")).permitAll()
            .antMatchers(this.adminServer.path("/login")).permitAll().anyRequest().authenticated() 
    ).formLogin(
        (formLogin) -> formLogin.loginPage(this.adminServer.path("/login")).successHandler(successHandler).and() 
    ).logout((logout) -> logout.logoutUrl(this.adminServer.path("/logout"))).httpBasic(Customizer.withDefaults()) 
        .csrf((csrf) -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) 
            .ignoringRequestMatchers(
                new AntPathRequestMatcher(this.adminServer.path("/instances"),
                    HttpMethod.POST.toString()), 
                new AntPathRequestMatcher(this.adminServer.path("/instances/*"),
                    HttpMethod.DELETE.toString()), 
                new AntPathRequestMatcher(this.adminServer.path("/actuator/**")) 
            ))
        .rememberMe((rememberMe) -> rememberMe.key(UUID.randomUUID().toString()).tokenValiditySeconds(1209600));
  }
  // Required to provide UserDetailsService for "remember functionality"
  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication().withUser(security.getUser().getName())
        .password("{noop}" + security.getUser().getPassword()).roles("USER");
  }
}

Ahora al intentar acceder a la pantalla anterior veremos el formulario de acceso, en el cual deberemos indicar el usuario y contraseña indicados en el fichero de configuración:


Una vez que tenemos el servidor desplegado y securizado. Vamos a proceder a configurar un cliente que se pueda conectar a dicho servidor. La configuración es igual a la que hemos visto anteriormente:
  • Añadir la librería spring-boot-admin-starter-client.
  • En el fichero de configuración, añadimos las propiedades spring.boot.admin.client.username/password que permitan al cliente registrarse en el servidor.
  • En el fichero de configuración, indicar a través de la propiedad spring.boot.admin.client.url, la ruta donde estará nuestra aplicación. 
  • En el fichero de configuración, indicar a través de la propiedad management.endpoints.web.exposure.include la habilitación de los endpoints de Actuator. 
Y una vez que hemos arrancado la nueva aplicación, desde la pestaña Aplicaciones del menú de administración del SBA, podremos ver cuales son los clientes conectados al mismo. Además, clickeando sobre cualquiera de los clientes, podremos ver información detallada sobre dicha aplicación. 



Pero como estamos en un mundo donde los micro servicios dominan el panorama actual, ahora vamos a ver una configuración un poco más actual. Habilitando la configuración a través de un Service Discovery como Eureka. Para ello, crearemos un servidor Eureka, puedes ver el código de ejemplo aquí. 

Una vez que tenemos arrancado el servidor de Eureka, procederemos a realizar la configuración necesaria en el servidor de SBA. Para ello seguimos los siguientes pasos:
  • Añadimos la dependencia spring-cloud-starter-netflix-eureka-client, que nos permitirá conectarnos con el servidor de Eureka. Por tanto ya no es necesaria que utilicemos la librería spring-boot-admin-starter-client, ni las propiedades con el sufijo spring.boot.admin en el fichero de configuración.
  • Añadimos la dependencia spring-boot-starter-actuator en caso de que este como dependencia pasiva de otra librería. 
  • En el fichero de configuración, indicamos a través de la URL por defecto del servidor en la propiedad eureka.client.serviceUrl.defaultZone.
  • En el fichero de configuración, habilitamos que Eureka conozca el estado de la aplicación a través del endpoint health de la aplicación con la propiedad eureka.client.healthcheck.enabled.
  • En el fichero de configuración, indicamos las credenciales de seguridad para acceder a los endpoints securizados a través de las propiedades eureka.instance.metadata-map.user.name/password. Este paso solo es necesario si los endpoints se encuentran securizados.
Si hemos seguido los pasos correctamente tanto en el servidor SBA y en los clientes, al acceder a Eureka podremos visualizarlos los dos:


Y si accedemos al servidor SBA también podremos visualizar ambas aplicaciones en el mismo. 

Como siempre, si tenemos los conceptos más o menos claros, montar una aplicación con Spring Boot es bastante sencillo. Si quieres acceso al código visita el siguiente enlace. 

lunes, 11 de octubre de 2021

Apache Camel: Control de Trazas con Zipkin

Hoy vamos a ver posiblemente uno de los post más sencillos que hemos hecho de Apache Camel. En el veremos como integrarlo con Zipkin. 

Zipkin es una herramienta distribuida de instrumentación y monitorización que nos permitirá recopilar información de servicios y realizar búsquedas sobre la misma. Con el objetivo de hallar problemas en la latencia de los mismos, a través principalmente de evaluar cuanto tiempo ha tardado en la ejecución del servicio. 

Ya vimos hace tiempo un ejemplo de Apache Camel con Jaeger, aquí. Y esta es otra herramienta pero con un objetivo similar. Por lo que nos servirá refrescar conceptos que vimos en el anterior post:

  • Distributed Tracing: Es un método utilizado para perfilar y supervisar las aplicaciones. Nos ayuda a señalar dónde ocurren los fallos y qué es lo que causa el mal funcionamiento.
  • Span: Es el principal componente de una traza distribuida, que representa una unidad individual de trabajo realizada en un sistema distribuido. Estos encapsulan información de la aplicación.
  • Traza: Es un gráfico acíclico de Span. 

Camel Zipkin es un componente que nos permitirá rastrear los mensajes de entrada y salida a Camel y su tiempo asociado. Los span serán capturado y enviados a Zipkin. 

Uno de los puntos importantes, es que se cree un mapeo de los endpoints de Apache Camel para que se pueda realizar mejores búsquedas en Zipkin. Asociando cada span a un servicio concreto y no a toda la aplicación. 

El mapeo de los servicios se puede realizar con el uso de wildcards o expresiones regulares. Si no se indica nada, Camel mandará por defecto la URL de las invocaciones. 

Pero empecemos con el ejemplo, el cual podremos ver en marcha facilmente. El primer paso será levantar un docker con zipkin. Lo cual podemos hacer en una sola linea. 

docker run -d -p 9411:9411 openzipkin/zipkin

Y el siguiente paso será configurar nuestra aplicación. Como efecto diferenciador a cualquier otra aplicación de Apache camel tendremos que añadir la dependencia para hacer uso de Zipkin. Como nosotros utilizamos Apache Camel con Spring, solo necesitaremos el starter. 

<dependency>
	<groupId>org.apache.camel.springboot</groupId>
	<artifactId>camel-zipkin-starter</artifactId>
</dependency>

Lo siguiente será configurar la aplicación para el envío de información a Zipkin a través de la anotación @CamelZipkin que pondremos en la clase principal de arranque. Y en el fichero de configuración indicaremos la URL donde se enviarán los spans.

camel.zipkin.endpoint=http://localhost:9411/api/v2/spans

Ahora ya podremos realizar invocaciones a los métodos de nuestros servicios y veremos dichas invocaciones en la interfaz gráfica de Zipkin. 


Como se puede apreciar en la captura, se indica el tiempo de ejecución de ejecución del método. Pero como no hemos configurado nada, vemos simplemente la URL de la invocación que hemos hecho. 

Si pulsamos en el botón show, podremos ver más detalle del span, las etiquetas asociadas y la traza asociada a la misma. 


La información que vemos es un poco escueta, porque no hemos realizado ningún tipo de configuración ni mapeo de los servicios. A continuación veremos como podemos personalizar la medición de las invocaciones a los métodos. 

En el fichero application.properties podemos indicar dos tipos de configuración. Son incompatibles, si se marca el mapeo individual, los servicios que nos se encuentren mapeados no se enviarán a zipkin. 

  • camel.zipkin.service-name: Nos permite indicar un nombre para el mapeo de todos los servicios de la aplicación y que no se muestre únicamente la URL. 
  • camel.zipkin.server-service-mappings.{serviceId}: Nos permite indicar un mapeo específico a un servicio concreto. Este servicio se puede identificar por el routeId del mismo en Camel. 
Si indicamos la siguiente configuración, tendremos la salida que mostramos más abajo. 

camel.zipkin.server-service-mappings.mockClientGetAll=getAll
camel.zipkin.server-service-mappings.mockGetById=getById


Como veis, ha sido realmente sencillo comunicar nuestros servicios de Apache Camel con una plataforma tan útil como Zipkin e instrumentalizar y monitorizar el funcionamiento de vuestros micro-servicios. 

Como siempre todo el código podéis verlo aquí. 

domingo, 25 de abril de 2021

Gestión del rendimiento de las aplicaciones con InspectIT

Hoy vamos a ver una herramienta open source para realizar diferentes tipos de tareas, desde tracing, monitorización o instrumentación: inspectIT. Esta herramienta es un Java Agent, los cuales ya conocemos algo mejor por el último post que hicimos, y que puedes ver aquí. 

Este software intenta minimizar dos cosas. Por un lado, el tiempo de configuración necesario para utilizar otras herramientas de Application Performance Management, APM. Tal y como pueden ser Jaeger, Prometheus, Zipkin o Micrometer. Y por otro reducir las modificaciones a realizar en nuestras aplicaciones si queremos realizar tareas de instrumentación. 

InspectIT esta basado en OpenCensus, que es un conjunto de librerías open software realizada en distintos lenguajes que permiten obtener métricas y trazas distribuidas. Dicho software tiene ya una amplia gama de exporters que permiten enviar la información a otras herramientas de monitorización. Y inspectIT soporta la mayoría de estos exporters. 

Por todo ello, inspectIT es una herramienta genial. Al ser un Java Agent no necesitaremos modificar nuestras aplicaciones para obtener información de su rendimiento. Y además permite obtener métricas y trazas de forma automática y enviarlas a distintos destinos. 

A través de los ejemplos, podremos ver como obtener las trazas y métricas de nuestra aplicación. El funcionamiento será similar en los distintos ejemplos. Para poder hacerlo funcionar deberemos seguir los dos siguientes pasos:

  • Asociar el java agent almacenado en la librería a través del argumento de la JVM '-javaagent:/full/path/to/library'.
  • Asociar a través de un argumento de la JVM la propiedad del exporter a utilizar. 
Para el primer ejemplo, usaremos un exporter de Jaeger, con el cual podremos obtener las trazas de la aplicación. La base del mismo será uno de nuestros ejemplos que hemos hecho en post anteriores, basados en Apache Camel, y que puedes ver aquí. Cabe recordar que Jaeger es una herramienta o Tracer que nos permitirá recolectar la información de las trazas de nuestra aplicación y visualizarlas en un interfaz gráfica propia 

Los pasos son los siguientes: 
  • Por un lado, tenemos que hacer es descargarnos la librería que contiene el java agent, lo podemos hacer desde aquí. Y la ubicamos en una ruta que indicaremos más adelante al arrancar la aplicación. 
  • Arrancar Jaeger, por ejemplo con un docker-compose como el siguiente.
version: '2.4'
networks:
  sandbox-apache-net:
    ipam:
      driver: default
      config:
        - subnet: 172.24.0.0/16

services:
  mysql:
    image: mysql:5.7.26
    mem_limit: 2G
    container_name: sandbox-apache-mysql
    hostname: sandbox-apache-mysql
    networks:
      sandbox-apache-net:
        ipv4_address: 172.24.1.1
    environment:
      MYSQL_HOST: sandbox-apache-mysql
      MYSQL_ROOT_PASSWORD: root
    ports:
      - 3306:3306
    volumes:
      - ./configs/mysql/conf.d/custom.cnf:/etc/mysql/conf.d/custom.cnf
      - ./configs/mysql/scripts:/docker-entrypoint-initdb.d

  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - 16686:16686
      - 14268:14268

  • Por último arrancar la JVM pasandole los argumentos que nos permitan configurar el exporter deseado. Para el caso de Jaeger usaremos los siguientes:
-javaagent:/home/deesebc/Downloads/inspectit-ocelot-agent-1.8.1.jar \
-Dinspectit.exporters.tracing.jaeger.url=http://127.0.0.1:14268/api/traces \
-Dinspectit.exporters.tracing.jaeger.service-name=inspectitJaegerExample

En la propiedad URL indicamos la dirección donde esta ubicado Jaeger, y con service-name podemos indicar el nombre con el cual registraremos las trazas de Jaeger. 

Como paso previo a ver el funcionamiento en Jaeger, vamos ha hacer varias consultas a nuestra aplicación. Para que así puedan ser registradas por inspectIT. Si usas el código de mi aplicación, será una invocación similar a esta: http://localhost:9090/book/1.

Si accedemos a la URL http://localhost:16686/search podremos acceder al buscador de trazas y buscar por las de nuestra aplicación.


La siguiente parte del ejemplo, para ver más del potencial de inspectIT y ver su funcionamiento para obtener métricas haremos uso de los exporters para Prometheus. El ejemplo en el que nos basaremos, será este. 

Empezamos de nuevo preparando un docker-compose que nos permita desplegar Prometheus. Podemos basarnos en el del ejemplo anterior y sería algo similar a esto:

version: '2.4'
networks:
  sandbox-apache-net:
    ipam:
      driver: default
      config:
        - subnet: 172.24.0.0/16

services:
  mysql:
    image: mysql:5.7.26
    mem_limit: 2G
    container_name: sandbox-apache-mysql
    hostname: sandbox-apache-mysql
    networks:
      sandbox-apache-net:
        ipv4_address: 172.24.1.1
    environment:
      MYSQL_HOST: sandbox-apache-mysql
      MYSQL_ROOT_PASSWORD: root
    ports:
      - 3306:3306
    volumes:
      - ./configs/mysql/conf.d/custom.cnf:/etc/mysql/conf.d/custom.cnf
      - ./configs/mysql/scripts:/docker-entrypoint-initdb.d
      
  prometheus:
    image: prom/prometheus:v2.6.1
    ports:
      - 9091:9090
    volumes:
      - ./configs/prometheus.yml:/etc/prometheus/prometheus.yml
    networks:
      sandbox-apache-net:
        ipv4_address: 172.24.1.2
    extra_hosts:
      docker.host: 172.28.0.1

172.28.0.1 es la IP de la interfaz bridge de docker. 

El siguiente punto será arrancar nuestra aplicación con los argumentos que nos permitan exportar la información a Prometheus. 

-javaagent:/home/deesebc/Downloads/inspectit-ocelot-agent-1.8.1.jar \
-Dinspectit.exporters.metrics.prometheus.host=172.28.0.1 \
-Dinspectit.exporters.metrics.prometheus.port=8888

Recordemos que para que Prometheus funcione, tiene que leer de una URL información sobre la aplicación. Por lo que por un lado debemos configurar, que Prometheus lea de un punto específico. Y esto lo haremos a través del fichero de configuración de Prometheus (parte del docker-compose):

scrape_configs:
  - job_name: 'prometheus'
    scrape_interval: 1m
    static_configs:
      - targets: ['localhost:9090']
  - job_name: 'apache_example'
    scrape_interval: 1m
    metrics_path: '/metrics'
    static_configs:
      - targets: ['172.28.0.1:8888']

Y a través de los argumentos JVM, indicaremos a inspectIT que queremos exponer la información de nuestra aplicación en la IP y puerto indicados. Algo que en el ejemplo en el cual nos basamos era realizado por Spring Actuator.  

Una vez con todo arrancado, realizaremos un par de invocaciones como las comentadas anteriormente. Y ya podremos acceder a Prometheus y ver la monitorización que se ha realizado del mismo. 

Si accedemos a la ruta http://localhost:8888/metrics podremos ver las distintas métricas que estamos generando desde la aplicación. Y si accedemos a la URL de Prometheus http://localhost:9091/targets podremos observar si nuestro endpoint esta siendo leído correctamente por Prometheus.


El siguiente paso será acceder a http://localhost:9091/graph y realizar algún gráfico en función de las métricas obtenidas. Por ejemplo uno que cuente las invocaciones que realizamos a endpoints de nuestra aplicación, a través del parámetro http_in_count.


Como veis, realmente facil. Ya no hay excusas para realizar tareas de gestión del rendimiento de las aplicaciones con InspectIT. Y esto han sido un par de ejemplos, pero podréis hacer más cosas a través de los distintos exporters. 

sábado, 10 de abril de 2021

Java Agents e Instrumentación: Tutorial

Últimamente hemos estado viendo en los posts como realizar tareas de monitorización. Hoy veremos los Java Agents, que son y como funcionan. Los cuales también pueden ayudarnos a realizar tareas de monitorización, entre otras cosas. Algo que intentaremos explicar en este post, primero con un poco de teoría y luego con la ayuda de ejemplos. 

¿Que son los Java Agents?, son básicamente librerías Java en las cuales se incluyen clases que implementan la API de Instrumentación de Java. La cual esta disponible desde la JDK 1.5. Esta es una API muy sencilla pero a la vez muy potente. Y establece las bases de la instrumentación de aplicaciones. 

La funcionalidad principal de esta instrumentación, es que se nos permitirá la adición de código a determinados métodos. Y con esta adición, podremos recompilar datos para ser utilizados con fines principalmente de monitorización y análisis o registro de eventos. Permitiéndonos obtener información vital sobre la aplicación que nos puede ayudar a resolver problemas u obtener datos que no serían posibles de otra forma. 

La principal cualidad de esta API de instrumentación es la alteración del byte-code de las clases que están siendo ejecutadas por la maquina virtual. A través de esta alteración podremos realizar las acciones anteriormente indicadas. 

Pero centrémonos, ¿Que necesitamos para crear un Java Agent?, seguir estos tres pasos:

  1. Desarrollar un método concreto.
  2. Crear y configurar correctamente el fichero MANIFEST.MF
  3. Indicar a la máquina virtual que tenga en cuenta a nuestra librería con el Java Agent. 

El método a desarrollar depende de cuando queramos que sea asociado el agente a la ejecución de aplicaciones en la JVM. La API de instrumentación pondrá a disposición de nosotros dos métodos diferentes:

  • premain: Permitirá la ejecución del Java Agent y su método asociado antes de la ejecución de cualquier otra aplicación en la JVM. Para lo cual el Java Agent debe indicarse en el arranque de la JVM. 
  • agentmain: Permitirá la ejecución del Java Agent una vez que la JVM ya se encuentre ejecutando una aplicación. Esta asociación debe realizarse de forma programática. 
La creación del fichero MANIFEST.MF debe realizarse manualmente, aunque se puede incluir en el jar asociado a través de un plugin de maven. En este fichero deberemos configurar distintas propiedades. Por un lado cual es la clase que contiene al método del Java Agent y propiedades acerca del comportamiento del mismo. Las principales son:
  • Premain-Class: Para indicar la clase que implemente el método premain.
  • Agent-Class: Para indicar la clase que implemente el método agentmain.
  • Can-Redefine-Classes: Permite indicar si el Java Agent puede o no redefinir las clases que interfiera.
  • Can-Retransform-Classes: Permite indicar si el Java Agent puede o no transformar la clase que interfiera. 
Una vez que tenemos desarrollado el método que realice la lógica y hayamos configurado el fichero MANIFEST correctamente, debemos generar la librería. Para ello podemos hacer uso de un par de plugins, los cuales nos facilitarán el trabajo:
  • maven-shade-plugin: nos permite crear la librería incluyendo las dependencias en el caso de que las tenga. 
  • maven-jar-plugin: nos permite asociar el fichero MANIFEST a la librería a crear. 
<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <executions>
            <execution>
                <phase>package</phase>
                <goals>
                    <goal>shade</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.2.0</version>
        <configuration>
            <archive>
                <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
            </archive>
        </configuration>
    </plugin>
</plugins>

El tercer paso, una vez tengamos la librería, será asociarla a la JVM. Tal y como hemos indicado anteriormente, se puede realizar de dos formas:

Si hemos implementado el método premain y configurado la propiedad Premain-Class, deberemos indicar la librería en el momento de arrancar la aplicación que queramos interferir. Ejemplo:

java -javaagent:/full/path/to/javaAgent.jar -jar app.jar

En el caso de que implementemos el método agentmain y configurado la propiedad Agent-Class, deberemos asociar el Java Agent de forma programática a través de la Java Attach API.

File agentFile = Paths.get("agent.jar").toFile();
VirtualMachine jvm = VirtualMachine.attach(VirtualMachine.list().get(0).id());
jvm.loadAgent(agentFile.getAbsolutePath());
// jvm.detach(); //to finish association

Ahora ya sabemos la teoría por lo que podemos ir a la practica. Vamos a ver un par de ejemplos. Uno sencillo y básico y otro algo más complejo. 

En el ejemplo más sencillo simplemente implementaremos el método premain, y veremos un par de logs asociados a su ejecución. 

@Log4j2
public class BasicAgentExample {
	public static void premain(final String agentArgs, final Instrumentation inst) {
		log.info("Start from premain. agentArgs: " + agentArgs);
		inst.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> {
			log.info(String.format("Classname: %s", className));
			return null;
		});
	}
}

Si ejecutamos un método static void main, por ejemplo, y añadimos como argumento el java agent, podremos ver una salida similar a esta:

[INFO ] 2021-03-04 [main] BasicAgentExample - Start from premain. agentArgs: null
[INFO ] 2021-03-04 [main] BasicAgentExample - Classname: sun/misc/PostVMInitHook
....

Como podemos ver, si miramos un poco los métodos disponibles en el objeto Instrumentation, tendremos la posibilidad de transformar o redefinir el funcionamiento de una clase. Pero este debe realizarse con operaciones complejas de modificación de los bytes de las clases. 

En el siguiente ejemplo, algo más complejo, transformaremos la clase que vayamos a interferir. Pero como hemos indicado estas operaciones son complejas. Y para llevarlas a cabo más fácilmente haremos uso de la librería Byte Buddy.  

La idea de este ejemplo, es crear un Java Agent que nos permita medir cuantas veces se ha invocado un método determinado. Aparte, también filtraremos que clases queremos intervenir, evitando que se realicen tareas sobre clases que no nos interesan, como por ejemplo las de la JVM. En nuestro ejemplo intervendremos solo aquellas clases que extiendan de una clase abstracta. 

Primero implementamos el método premain

@Log4j2
public class CounterAgentExample {
  public static void premain(final String agentArgs, final Instrumentation inst) {
    log.info("Start from premain. agentArgs: " + agentArgs);
    Advice advice = Advice.to(CounterMeasureAdvice.class);
    new AgentBuilder.Default()
      .type(ElementMatchers.hasSuperType(ElementMatchers.isAbstract()))
      .transform((builder, type, classLoader, module) -> builder.visit(advice.on(ElementMatchers.isMethod())))
      .installOn(inst);
  }
}

El constructor AgentBuilder nos permitirá indicar que elementos queremos intervenir a través del método type y de la utilidad ElementMatchers. El método transform nos permitirá indica sobre que elemento de esa clase queremos realizar la transformación, en este caso sobre los métodos de las clases que cumplan hayan pasado el filtro. 

public class CounterMeasureAdvice {
  public final static Logger log = LogManager.getRootLogger();
  public static Map<String, Long> map = new HashMap<>();

  @Advice.OnMethodEnter
  public static void enter(@Advice.Origin final String origin) {
	log.info("CounterMeasureAdvice: Enter into: " + origin);
	if (map.containsKey(origin)) {
		map.put(origin, map.get(origin) + 1);
	} else {
		map.put(origin, 1L);
	}
	log.info(String.format("Origin %s invoked %d times", origin, map.get(origin)));
  }
}

Crearemos tambien un advice, que es un elemento asociado a la programación de aspecto, que nos permite ejecutar nuestro código en un punto específico. Los advice que creemos pueden ser de distinto tipo, en función de cuando queremos que se tengan en cuenta respecto a ese punto fijo: antes, o después de ese determinado punto. Para el ejemplo, ejecutaremos nuestro advice antes de acceder a los métodos que cumplan los filtros. 

public class ExampleThree extends AbstractClass {
  public static int fibonacci(final int n) {
	if (n > 1) {
		return fibonacci(n - 1) + fibonacci(n - 2); // función recursiva
	} else { // caso base
		return n;
	}
  }
  // java -javaagent:/path/to/agent.jar -cp *.jar the.main.ClassName
  public static void main(final String[] args) {
	System.out.println("Fibonacci de 5: " + fibonacci(5));
  }
}

Para probar nuestro Java Agent, ejecutaremos la clase que se muestra arriba. Indicando correctamente la librería del Java Agent y la clase que implementa dicho agente en el fichero MANIFEST. La salida será similar a esta:

[INFO ] 2021-03-04 [main]  - CounterMeasureAdvice: Enter into: public static int ExampleThree.fibonacci(int)
[INFO ] 2021-03-04 [main]  - Origin public static int ExampleThree.fibonacci(int) invoked 14 times
[INFO ] 2021-03-04 [main]  - CounterMeasureAdvice: Enter into: public static int ExampleThree.fibonacci(int)
[INFO ] 2021-03-04 [main]  - Origin public static int ExampleThree.fibonacci(int) invoked 15 times

Como podéis ver, el uso de Java Agent tiene muchas posibilidades. Incluso con la librería de Byte Buddy se pueden hacer cosas complejas de forma sencilla.