Criando um serviço com spring graalvm leve e com monitoramento configurado
Introdução
Esse tutorial tem o objetivo de transformar uma aplicação web spring em código nativo usando graalvm e integrando com o grafana usando o open telemetry.
Pré Requisitos
Ter experiência com o spring, grafana e com o docker porque só vamos falar sobre como funciona o build na graalvm e como integrar com o grafana todo o resto eu vou assumir que você já sabe.
O que é graalvm ?
É uma JDK de alta performance que usa um JIT alternativo e troca também o algoritmo do garbage collector para ficar mais performática. Outra coisa legal é que depois de compilado esse binário pode ser executado por qualquer imagem de slim de linux. A desvantagem é que a gente perde todas as facilidades da JVM padrão como o javaagent.
Setup do grafana no docker
Crie um arquivo docker-compose.yml e coloque esse conteúdo abaixo, nesse docker tem o grafana o prometheus e o collector que é o carinha que nós vamos nos integrar para enviar os dados para o grafana. Copie os arquivos da pasta docker do meu repositório não vou focar em como eles funcionam Link da pasta.
services:
collector:
container_name: collector
image: otel/opentelemetry-collector-contrib:0.91.0
command:
- --config=/etc/otelcol-contrib/otel-collector.yml
volumes:
- ./docker/collector/otel-collector.yml:/etc/otelcol-contrib/otel-collector.yml
ports:
- "4317:4317" # OTLP gRPC receiver
# - "4318:4318" # OTLP HTTP receiver
- "8889" # Prometheus exporter metrics
depends_on:
- loki
- jaeger-all-in-one
- zipkin-all-in-one
- tempo
networks:
- spring_native_network
tempo:
container_name: tempo
image: grafana/tempo:latest
command: [ "-config.file=/etc/tempo.yml" ]
volumes:
- ./docker/tempo/tempo.yml:/etc/tempo.yml
ports:
- "4317" # otlp grpc
- "3200" # tempo as grafana datasource
networks:
- spring_native_network
loki:
container_name: loki
image: grafana/loki:latest
command: -config.file=/etc/loki/local-config.yaml
ports:
- "3100"
networks:
- spring_native_network
prometheus:
container_name: prometheus
image: prom/prometheus
volumes:
- ./docker/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
command:
- --config.file=/etc/prometheus/prometheus.yml
- --enable-feature=exemplar-storage
- --web.enable-remote-write-receiver
ports:
- '9090:9090'
depends_on:
- collector
networks:
- spring_native_network
grafana:
container_name: grafana
image: grafana/grafana
volumes:
- ./docker/grafana/grafana-datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml
ports:
- "3000:3000"
depends_on:
- prometheus
- loki
- jaeger-all-in-one
- zipkin-all-in-one
- tempo
networks:
- spring_native_network
jaeger-all-in-one:
container_name: jaeger
image: jaegertracing/all-in-one:latest
environment:
- COLLECTOR_OTLP_ENABLED=true
ports:
- "16686:16686"
- "4317"
networks:
- spring_native_network
zipkin-all-in-one:
container_name: zipkin
image: openzipkin/zipkin:latest
ports:
- "9411:9411"
networks:
- spring_native_network
networks:
spring_native_network:
driver: bridge
Agora rode docker compose up e veja se todos os conteiners subiram.
Criando a App do spring
Crie uma app spring normal adicione a dependência do spring native e spring-boot-starter-web depois que o projeto foi criado adicione as dependências do opentelemetry.
dependencyManagement {
imports {
mavenBom("io.opentelemetry:opentelemetry-bom:1.41.0")
mavenBom("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:2.7.0")
}
}
dependencies {
implementation 'io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter'
}
Depois de baixar a dependencia configure o endpoint do coletor de logs/trace do grafana, nele nós podemos escolher qual protocolo vamos usar grpc/http e os atributos que seram enviados para o grafana como nome da app, env e namespace.
otel:
instrumentation:
spring-web:
enabled: true
exporter:
otlp:
protocol: grpc
endpoint: http://localhost:4317
# protocol: http/protobuf
# endpoint: http://localhost:4318 # se for usar o http tem que trocar a porta para 4318
logs:
exporter: none
resource:
attributes:
service.name: spring-service
service: spring-service
env: dev
namespace: B.O
Crie um application-prod.yml onde ficará apenas o endpoint que será usado no ambiente do docker assim a gente consegue testa com o localhost na idea e fazer o build com a url que vai funcionar certo no conteiner.
otel:
exporter:
otlp:
endpoint: http://collector:4317
Dockerfile e compilação
Na compilação do spring native ele não pode ter um profile default como local ou dev porque ele seta o profile na compilação nesse caso ele vai setar o default e quando a gente for adicionar o profile no docker-compose ele vai subistituir pelo prod, mais se já tivesse outro por exemplo dev ele ficaria com dois profiles depois de compilado.
Então se a sua ideia é passar o profile na compilação pode passar a env SPRING_PROFILES_ACTIVE como argumento do docker build na pipeline e ele vai setar o profile.
Nós vamos usar um dockerfile com multiplos estagios porque depois de compilado o spring não vai precisar mais da jvm para rodar e ele pode ser executado como um binario de maquina o que pode tornar o conteiner mais leve.
# Imagem da graalvm
FROM ghcr.io/graalvm/graalvm-community:21 AS build
# não fazer isso em prod pode dar ruim
USER root
# copiando os arquivos do projeto para dentro do conteiner
# copie só o necessario para o build assim vc evita problemas
COPY settings.gradle .
COPY build.gradle .
COPY src src
COPY gradlew gradlew
COPY gradle gradle
# compilando sem rodar os testes na compilação a ideia é q rode os testes antes na pipeline de deploy
RUN ./gradlew nativeCompile -x test
# usando uma imagem leve apenas para rodar o binario poderia ser qualquer outro linux
FROM debian:12-slim
# ENV SPRING_PROFILES_ACTIVE=prod
# copiando o binario do estagio de build para o estagio final
COPY --from=build /build/native/nativeCompile/* .
EXPOSE 8080
# rodando o programa o nome do binario pode mudar dependendo do nome do projeto compile local para ver o nome e evitar erros
CMD ["./Spring"]
Agora vamos adicionar o nosso serviço no docker-compose para fazer o build e ver se ele vai enviar os dados para o grafana.
services:
spring-native-api:
container_name: spring-native-api
build:
context: .
dockerfile: ./docker/Dockerfile
environment:
- SPRING_PROFILES_ACTIVE=prod
ports:
- "8080:8080"
depends_on:
- collector
networks:
- spring_native_network
Agora é só fazer o build e ser feliz com o novo conteiner leve usando o que tem de mais massa no spring.
Se estiver usando o spring 3.3 e o java 21 tambem podemos habilitar as virtual threads no framework spring.threads.virtual.enabled=true.
Conclusão
Espero que isso ajude o seu time dependendo do contexto tem um ganho de performace no uso de memoria que é bizarro, qualquer duvida manda ai vou deixa o link do repositorio do github em baixo.
https://github.com/Claudio-code/spring-native-open-telemetry