Por desgracia, puede resultar caro externalizar esta responsabilidad; sin embargo, existen soluciones de código abierto, y una de las mejores es la plataforma Grafana. Grafana es la herramienta de visualización para una variedad de fuentes de datos, pero el equipo de Grafana también tiene sus propias fuentes de datos para registros (Loki), métricas (Mimir) y trazas (Tempo). En este artículo veremos cómo conectar una aplicación Spring Boot a este ecosistema.
Nuestro primer paso es configurar nuestro entorno de observabilidad. Utilizaremos docker compose para crear este entorno con este archivo docker-compose.yaml:
version: "3.9"
networks:
telemetry:
volumes:
influxdb-storage:
grafana-storage:
services:
grafana:
image: grafana/grafana:9.3.1
depends_on:
- influxdb
volumes:
- ./docker/grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml
- grafana-storage:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin1
- GF_SERVER_HTTP_PORT=3000
- INFLUXDB_HOST=influxdb
- INFLUXDB_PORT=8086
- INFLUXDB_NAME=db0
- INFLUXDB_USER=influxuser
- INFLUXDB_PASS=influxuser1
ports:
- "3000:3000"
networks:
- telemetry
influxdb:
image: influxdb:latest
ports:
- '8086:8086'
volumes:
- influxdb-storage:/var/lib/influxdb
environment:
- INFLUXDB_URL=http://influxdb:8086
- INFLUXDB_ADMIN_USER=influxuser
- INFLUXDB_ADMIN_PASSWORD=influxuser1
loki:
image: grafana/loki:2.7.1
ports:
- "3100:3100"
command: -config.file=/etc/loki/loki.yaml
volumes:
- ./docker/loki.yaml:/etc/loki/loki.yaml
networks:
- telemetry
tempo:
image: grafana/tempo:1.5.0
command: [ "-config.file=/etc/tempo.yaml" ]
volumes:
- ./docker/tempo.yaml:/etc/tempo.yaml
- ./data/tempo:/tmp/tempo
ports:
- "14268:14268" # jaeger ingest
- "3200:3200" # tempo
- "55680:55680" # otlp grpc
- "55681:55681" # otlp http
- "9411:9411" # zipkin
- "4318:4318" # new http
- "4317:4317" # new grpc
networks:
- telemetry
mimir:
image: grafana/mimir:2.5.0
command: "-config.file=/etc/mimir/mimir.yaml"
ports:
- "9009:9009"
volumes:
- "./docker/mimir.yaml:/etc/mimir/mimir.yaml"
- "/tmp/mimir/rules:/tmp/mimir/rules"
networks:
- telemetry
Aquí puedes ver que estamos creando una instancia de Grafana respaldada por una Influxdb junto con instancias de Loki, Tempo y Mimir. Exponemos los puertos apropiados para configurar cada instancia con los archivos correspondientes de nuestra carpeta docker. Lo más interesante es el grafana-datasources.yaml que dice grafana configura para conectarse a Loki, Mimir, y Tempo.
apiVersion: 1
datasources:
- name: Tempo
type: tempo
access: proxy
orgId: 1
url: http://tempo:3200
basicAuth: false
isDefault: true
version: 1
editable: false
apiVersion: 1
uid: tempo
- name: Loki
type: loki
access: proxy
orgId: 1
url: http://loki:3100
basicAuth: false
isDefault: false
version: 1
editable: false
apiVersion: 1
jsonData:
derivedFields:
- datasourceUid: tempo
matcherRegex: (?:traceID|trace_id)=(\w+)
name: TraceID
url: $${__value.raw}
- name: Mimir
type: prometheus
access: proxy
orgId: 1
url: http://mimir:9009/prometheus
isDefault: false
version: 1
editable: true
Una vez que ejecutamos "docker compose up" podemos acceder a localhost:9000 e iniciar sesión en Grafana con nuestras credenciales admin/admin1 definidas en el archivo docker compose. Ahora vamos a trabajar en conseguir algunos datos de nuestra aplicación Spring Boot. Empezaremos añadiendo logs a loki.
Registros
Por defecto, Spring Boot incluye slf4j por lo que no necesitamos añadir nada a nuestro build.gradle. Sin embargo, en este ejemplo, utilizaremos Grafana Agent para extraer nuestros logs y exportarlos a Loki, por lo que necesitamos actualizar nuestro archivo application.properties para escribir nuestros logs en un archivo y establecer un nivel de logging raíz como este:
logging.file.name=logs/app.log
logging.level.root=INFO
A continuación añadimos el Grafana Agent a nuestro archivo docker-compose.yaml:
grafana-agent:
image: grafana/agent:v0.22.0
volumes:
- ./docker/grafana-agent.yaml:/etc/agent-config/grafana-agent.yaml
- ./logs/:/var/log/
entrypoint:
- /bin/agent
- -config.file=/etc/agent-config/grafana-agent.yaml
- -prometheus.wal-directory=/tmp/agent/wal
ports:
- "12345:12345"
networks:
- telemetry
extra_hosts:
- "host.docker.internal:host-gateway"
Y le decimos a Grafana Agent cómo raspar nuestros logs configurando su archivo de configuración (grafana-agent.yaml).
server:
log_level: debug
http_listen_port: 12345
logs:
configs:
- name: default
positions:
filename: /tmp/localhost-positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: system
static_configs:
- labels:
job: localhostlogs
__path__: /var/log/*log
env: "local"
app: "observability-example"
Métricas
Para enviar nuestras métricas necesitamos incluir algunas dependencias en nuestro gradle.build
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
//enable /actuator/prometheus
runtimeOnly 'io.micrometer:micrometer-registry-prometheus'
//for timed aspect
implementation 'org.springframework:spring-aspects'
}
Ahora vamos a habilitar los endpoints de métricas en nuestra aplicación añadiendo algunas propiedades al application.properties:
management.endpoint.health.show-details=always management.endpoints.web.exposure.include=health,info,prometheus
Finalmente, le decimos a Grafana Agent de dónde leer nuestras métricas y dónde escribirlas actualizando el archivo de configuración grafana-agent.yaml:
management.endpoint.health.show-details=always management.endpoints.web.exposure.include=health,info,prometheus
Nuestro endpoint acutaor de la app Spring Boot contiene un montón de métricas geniales, si quieres ver ejemplos de algunas métricas personalizadas puedes mirar el FactorService en mi aplicación de ejemplo.
Trazas
Para añadir datos de trazas a nuestra aplicación necesitamos incluir algunas dependencias más en nuestro gradle.build
dependencies {
implementation("io.micrometer:micrometer-tracing")
implementation("io.micrometer:micrometer-tracing-bridge-otel")
implementation("io.opentelemetry:opentelemetry-exporter-zipkin")
}
Ahora vamos a habilitar la información de rastreo en nuestros logs y habilitar el rastreo y los endpoints en nuestra aplicación añadiendo algunas propiedades al application.properties:
logging.pattern.level="trace_id=%mdc{traceId} span_id=%mdc{spanId} trace_flags=%mdc{traceFlags} %p"
management.tracing.enabled=true
management.tracing.sampling.probability=1.0
management.zipkin.tracing.endpoint=http://localhost:9411
¡Ya está! Inicia la aplicación y relanza tu docker compose para que podamos recoger todos los cambios. Puedes encontrar todo el repositorio de la aplicación de ejemplo aquí https://github.com/scottbock/observability-example