Saltar al contenido principal

Observabilidad

Código Completo
El resultado final del código desarrollado en este documento se puede encontrar en el monorepo de GitHub springboot-demo-projects, bajo el tag observability.

La observabilidad se trata de entender qué está haciendo tu aplicación en producción sin tener que agregar print statements y redeployar. Cuando algo se rompe a las 3 de la mañana, necesitás rastrear requests entre servicios, ver logs de errores en contexto y entender cuellos de botella de performance sin adivinar.

Cómo funciona la instrumentación

El proceso se divide en tres pasos lógicos:

  1. Recolección de datos (La Aplicación): Modificás la app para incluir código especializado que recolecta varios tipos de datos sobre el estado interno de la app y el entorno del servidor host.

  2. Almacenamiento de datos (Los Backends de Telemetría): Los datos recolectados se envían fuera del proceso de la aplicación y van a backends de telemetría correspondientes y optimizados, bases de datos diseñadas específicamente para logs, métricas o traces.

  3. Visualización (El Dashboard): Usás una herramienta de visualización poderosa (como Grafana) para extraer los datos almacenados de los backends y presentarlos en dashboards coherentes y legibles.

Los tres pilares de los datos de telemetría

La instrumentación se enfoca en recolectar tres tipos distintos de datos, a menudo referidos como "Los Tres Pilares" de la observabilidad:

Tipo de DatoDefiniciónBackend de Telemetría Común
LogsRegistros de texto de eventos específicos o estados que ocurren dentro de la aplicación.Loki
MétricasPuntos de datos numéricos y agregados (ej., uso de CPU, conteos de latencia de requests, consumo de memoria).Prometheus
TracesEl viaje completo de un solo request a medida que fluye a través de las diversas partes de tu sistema.Tempo

Grafana actúa como el "único panel de vidrio" que unifica los tres tipos de datos. Es una plataforma de visualización basada en web que se conecta a múltiples backends de telemetría simultáneamente.

Estas son las opciones más comunes en el ecosistema de Grafana, pero no son las únicas. Las alternativas incluyen Elasticsearch para logs, InfluxDB para métricas, y Jaeger para traces.

Visión general de la arquitectura de observabilidad

Así es como las partes móviles se integran entre sí:

Scroll to zoom • Drag corner to resize

El flujo funciona así:

  • Las apps de Spring Boot exponen métricas vía Micrometer y envían traces vía OTLP a Tempo
  • Promtail scrapea los logs de contenedores Docker y los envía a Loki
  • Prometheus scrapea métricas del endpoint /actuator/prometheus de cada app
  • Grafana consulta los tres backends para mostrar dashboards unificados

A continuación, un resumen de los archivos nuevos y modificados:

Archivos a Crear/Modificar
File Tree
springboot-demo-projects/
├── build.gradle
├── docker-compose.yml
├── observability/
│ ├── grafana.Dockerfile
│ ├── grafana/
│ │ ├── dashboards/
│ │ │ ├── dashboards.yml
│ │ │ └── *.json
│ │ └── datasources/
│ │ └── datasources.yml
│ ├── loki.Dockerfile
│ ├── loki-config.yml
│ ├── prometheus.Dockerfile
│ ├── prometheus.yml
│ ├── promtail.Dockerfile
│ ├── promtail-config.yml
│ ├── tempo.Dockerfile
│ └── tempo.yml
└── src/
└── main/
└── resources/
└── application.yaml

Configuración del repositorio

Micrometer Registry Prometheus

Agregá micrometer-registry-prometheus para exponer métricas en formato Prometheus en /actuator/prometheus

build.gradle
// ...
dependencies {
// ...
implementation 'io.micrometer:micrometer-registry-prometheus:1.17.0-M2'
}
// ...

Configuración de la aplicación

Habilitá los endpoints de observabilidad y configurá dónde enviar los traces.

resources/application.yaml
# ...
management:
endpoints:
web:
exposure:
include: health,info,prometheus,metrics
endpoint:
health:
show-details: always
metrics:
enabled: true
prometheus:
enabled: true
prometheus:
metrics:
export:
enabled: true
metrics:
distribution:
percentiles-histogram:
http:
server:
requests: true
tags:
application: ${spring.application.name}
tracing:
sampling:
probability: 1.0
otlp:
tracing:
endpoint: http://tempo:4318/v1/traces
metrics:
export:
enabled: false

logging:
pattern:
level: "trace_id=%mdc{traceId} span_id=%mdc{spanId} trace_flags=%mdc{traceFlags} %p"
  • management.endpoints.web.exposure.include: Expone los endpoints de health, info, prometheus y metrics
  • management.tracing.sampling.probability: Configurá en 1.0 para tracear el 100% de los requests (reducir en producción)
  • management.otlp.tracing.endpoint: Envía traces a Tempo vía protocolo OTLP HTTP
  • logging.pattern.level: Incrusta el contexto de trace (trace_id, span_id, trace_flags) en cada línea de log

Configuración de la observabilidad

Configuración de Docker Compose

Agregá los servicios del stack de observabilidad a tu docker-compose.yml:

docker-compose.yml
services:
spring-java:
# ...
depends_on:
- tempo
networks:
- monitoring

spring-kotlin:
# ...
depends_on:
- tempo
networks:
- monitoring

spring-groovy:
# ...
depends_on:
- tempo
networks:
- monitoring

prometheus:
build:
context: .
dockerfile: observability/prometheus.Dockerfile
container_name: prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=15d'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
- '--web.enable-lifecycle'
volumes:
- prometheus-data:/prometheus
ports:
- "9090:9090"
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:9090/-/healthy"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
- monitoring

loki:
build:
context: .
dockerfile: observability/loki.Dockerfile
container_name: loki
ports:
- "3100:3100"
volumes:
- loki-data:/loki
command: -config.file=/etc/loki/local-config.yaml
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3100/ready"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
- monitoring

promtail:
build:
context: .
dockerfile: observability/promtail.Dockerfile
container_name: promtail
volumes:
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock
command: -config.file=/etc/promtail/config.yml
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:9080/ready"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
depends_on:
- loki
networks:
- monitoring

tempo:
build:
context: .
dockerfile: observability/tempo.Dockerfile
container_name: tempo
ports:
- "3200:3200"
- "4317:4317"
- "4318:4318"
volumes:
- tempo-data:/tmp/tempo
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3200/ready"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
- monitoring

grafana:
build:
context: .
dockerfile: observability/grafana.Dockerfile
container_name: grafana
environment:
- GF_SECURITY_ADMIN_USER=${GF_SECURITY_ADMIN_USER}
- GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD}
- GF_USERS_ALLOW_SIGN_UP=false
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
depends_on:
- prometheus
- loki
- tempo
networks:
- monitoring

networks:
monitoring:
driver: bridge

volumes:
prometheus-data:
driver: local
loki-data:
driver: local
grafana-data:
driver: local
tempo-data:
driver: local

Cada servicio de Spring Boot recibe dos adiciones:

  • depends_on: Asegura que Tempo inicie antes que las apps
  • networks: Se une a la red monitoring para que las apps puedan llegar a Tempo

El stack de observabilidad incluye:

  • Prometheus: Scrapea métricas de todos los servicios
  • Loki: Almacena e indexa datos de logs
  • Promtail: Recolecta logs de contenedores Docker y los reenvía a Loki
  • Tempo: Recibe y almacena traces distribuidos
  • Grafana: Visualiza métricas, logs y traces en dashboards unificados
En la práctica, los setups son diferentes

En algunos proyectos, es común encontrar los servicios de observabilidad viviendo en un proyecto de Docker Compose completamente separado, o incluso gestionados por proveedores terceros como Datadog o Grafana Cloud. Meter todo en un solo docker-compose.yml acá mantiene las cosas simples para la documentación y hace que el setup sea más fácil de seguir.

Loki

Loki es un sistema de agregación de logs horizontalmente escalable, altamente disponible y multi-tenant inspirado en Prometheus.

Dockerfile:

observability/loki.Dockerfile
FROM alpine:latest AS builder

RUN mkdir -p /loki/chunks /loki/rules

FROM grafana/loki:3.5.10

COPY --from=builder --chown=10001:10001 /loki /loki
COPY observability/loki-config.yml /etc/loki/local-config.yaml

USER 10001

Usa un build multi-stage para crear directorios con permisos correctos (Loki corre como usuario 10001).

Configuración:

observability/loki-config.yml
auth_enabled: false

server:
http_listen_port: 3100
grpc_listen_port: 9096

common:
instance_addr: 127.0.0.1
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory

query_range:
results_cache:
cache:
embedded_cache:
enabled: true
max_size_mb: 100

schema_config:
configs:
- from: 2020-10-24
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h

ruler:
alertmanager_url: http://localhost:9093

compactor:
working_directory: /loki/compactor
compaction_interval: 10m
retention_enabled: true
retention_delete_delay: 2h
retention_delete_worker_count: 150
delete_request_store: filesystem

limits_config:
retention_period: 360h # 15 days, matches Prometheus and Tempo

# By default, Loki will send anonymous usage data to Grafana.
# This can be disabled by setting this to false
analytics:
reporting_enabled: false
  • auth_enabled: false: Deshabilita la autenticación para desarrollo local
  • storage.filesystem: Usa almacenamiento de filesystem local (adecuado para configuraciones de un solo nodo)
  • retention_period: Mantiene logs por 15 días (360 horas)
  • analytics.reporting_enabled: false: Deshabilita el reporte de uso anónimo

Promtail

Promtail es un agente que envía el contenido de logs locales a Loki.

Dockerfile:

observability/promtail.Dockerfile
FROM grafana/promtail:3.5.10
COPY observability/promtail-config.yml /etc/promtail/config.yml

Configuración:

observability/promtail-config.yml
server:
http_listen_port: 9080
grpc_listen_port: 0

positions:
filename: /tmp/positions.yaml

clients:
- url: http://loki:3100/loki/api/v1/push

scrape_configs:
- job_name: containers
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
- source_labels: ['__meta_docker_container_label_com_docker_compose_service']
target_label: compose_service
- source_labels: ['compose_service']
regex: 'spring-.*'
action: keep
- source_labels: ['compose_service']
regex: 'spring-(.*)'
target_label: compose_service
replacement: 'spring_${1}'
pipeline_stages:
- regex:
expression: 'trace_id=\S+ span_id=\S+ trace_flags=\S+ (?P<type>\w+) \S+ ---'
- labels:
type:
  • docker_sd_configs: Descubre contenedores Docker automáticamente
  • relabel_configs: Filtra solo servicios spring-* y renombra labels
  • pipeline_stages: Parsea líneas de log para extraer el nivel de log y crear labels indexadas

El patrón regex trace_id=\S+ span_id=\S+ trace_flags=\S+ (?P<type>\w+) \S+ --- extrae el nivel de log de tu formato de log de Spring Boot, habilitando el filtrado por tipo de log (INFO, ERROR, DEBUG, etc.) en Grafana.

Tempo

Tempo es un backend de tracing distribuido de alto volumen con dependencias mínimas.

Dockerfile:

observability/tempo.Dockerfile
FROM alpine:latest AS builder

RUN mkdir -p /tmp/tempo/blocks /tmp/tempo/wal /tmp/tempo/generator/wal && \
chown -R 10001:10001 /tmp/tempo

FROM grafana/tempo:2.10.0

COPY --from=builder --chown=10001:10001 /tmp/tempo /tmp/tempo
COPY observability/tempo.yml /etc/tempo/tempo.yml

CMD ["-config.file=/etc/tempo/tempo.yml"]

Crea directorios requeridos con ownership apropiado antes de copiar el binario de Tempo.

Configuración:

observability/tempo.yml
auth_enabled: false

server:
http_listen_port: 3200

distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: "0.0.0.0:4317"
http:
endpoint: "0.0.0.0:4318"

ingester:
max_block_duration: 5m
trace_idle_period: 10s
max_block_bytes: 1_000_000

storage:
trace:
backend: local
wal:
path: /tmp/tempo/wal
local:
path: /tmp/tempo/blocks

query_frontend:
search:
duration_slo: 5s
throughput_bytes_slo: 1.073741824e+09

metrics_generator:
registry:
external_labels:
source: tempo
storage:
path: /tmp/tempo/generator/wal

overrides:
defaults:
metrics_generator:
processors: [service-graphs, span-metrics]

usage_report:
reporting_enabled: false
  • distributor.receivers.otlp: Acepta traces vía OTLP en los puertos 4317 (gRPC) y 4318 (HTTP)
  • storage.trace.backend: local: Usa filesystem local para almacenamiento de traces
  • metrics_generator: Habilita la generación de service graph y span metrics
  • usage_report.reporting_enabled: false: Deshabilita el reporte de telemetría

Prometheus

Prometheus es un toolkit de monitoreo y alerting de sistemas que recolecta y almacena sus métricas como datos de series temporales.

Dockerfile:

observability/prometheus.Dockerfile
FROM prom/prometheus:v3.9.1
COPY observability/prometheus.yml /etc/prometheus/prometheus.yml

Configuración:

observability/prometheus.yml
global:
scrape_interval: 60s
evaluation_interval: 60s

scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['prometheus:9090']

- job_name: 'spring-java'
static_configs:
- targets: ['spring-java:8080']
metrics_path: '/actuator/prometheus'

- job_name: 'spring-kotlin'
static_configs:
- targets: ['spring-kotlin:8080']
metrics_path: '/actuator/prometheus'

- job_name: 'spring-groovy'
static_configs:
- targets: ['spring-groovy:8080']
metrics_path: '/actuator/prometheus'
  • scrape_interval: Recolecta métricas cada 60 segundos
  • scrape_configs: Define tres jobs para scrapear métricas de cada servicio de Spring Boot
  • metrics_path: Apunta a /actuator/prometheus donde Micrometer expone las métricas

Grafana

Grafana provee visualización y análisis para tus datos de observabilidad.

Dockerfile:

observability/grafana.Dockerfile
FROM grafana/grafana:11.6.11
COPY observability/grafana/datasources /etc/grafana/provisioning/datasources
COPY observability/grafana/dashboards/dashboards.yml /etc/grafana/provisioning/dashboards/dashboards.yml
COPY observability/grafana/dashboards/*.json /var/lib/grafana/dashboards/

Copia la configuración de provisioning de datasources y dashboards en tiempo de build.

Configuración de Datasources:

observability/grafana/datasources/datasources.yml
apiVersion: 1

datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
uid: prometheus
isDefault: true
editable: false
jsonData:
httpMethod: POST
manageAlerts: true
exemplarTraceIdDestinations:
- datasourceUid: tempo
name: TraceID
urlDisplayLabel: "View Trace"

- name: Loki
type: loki
access: proxy
url: http://loki:3100
uid: loki
editable: false
jsonData:
derivedFields:
- name: TraceID
matcherRegex: "trace_id=(\w+)"
url: "$${__value.raw}"
datasourceUid: tempo
urlDisplayLabel: "View Trace"

- name: Tempo
type: tempo
access: proxy
url: http://tempo:3200
uid: tempo
editable: false
jsonData:
nodeGraph:
enabled: true
tracesToLogs:
datasourceUid: loki
filterByTraceID: true
filterBySpanID: false
tags:
- service.name

Configura tres datasources:

  • Prometheus: Para métricas, marcado como default
  • Loki: Para logs, con extracción de trace ID para correlación
  • Tempo: Para traces, con links de vuelta a logs de Loki

Las configuraciones exemplarTraceIdDestinations y derivedFields habilitan la correlación de trace a log. Cuando ves un pico de métrica, podés clickear para ver el trace; cuando ves logs, podés clickear el trace ID para ver el trace distribuido completo.

Configuración de Dashboards:

observability/grafana/dashboards/dashboards.yml
apiVersion: 1

providers:
- name: 'default'
orgId: 1
folder: ''
type: file
disableDeletion: false
updateIntervalSeconds: 10
allowUiUpdates: false
options:
path: /var/lib/grafana/dashboards
foldersFromFilesStructure: false

Habilita la carga automática de dashboards desde /var/lib/grafana/dashboards.

Dashboards Pre-configurados

El repositorio incluye dos dashboards pre-configurados adaptados de la comunidad de Grafana:

  • JVM Micrometer (

    dashboard 4701

    ): Métricas de JVM incluyendo memoria, threads, GC y carga de clases

  • Spring Boot Observability (

    dashboard 17175

    ): Métricas a nivel de aplicación con tasas de requests HTTP, tiempos de respuesta y tasas de error

Estos se omiten del patch debido a su tamaño (miles de líneas de JSON), pero podés encontrarlos en el repositorio en observability/grafana/dashboards/.

Deploy a producción con Coolify

Cuando deployás a Coolify, la plataforma detecta automáticamente los nuevos servicios definidos en tu docker-compose.yml y los inicia junto con tus aplicaciones de Spring Boot. No necesitás configurar manualmente el stack de monitoreo.

El único paso adicional es asignar un dominio a Grafana para que puedas acceder a los dashboards:

  1. En Coolify, encontrá el servicio de Grafana en tu proyecto
  2. Clickeá en él y configurá un dominio (ej., grafana.tudominio.com)
  3. Coolify se encargará de los certificados SSL y el routing
Variables de entorno de Grafana

Grafana espera que las variables de entorno GF_SECURITY_ADMIN_USER y GF_SECURITY_ADMIN_PASSWORD estén configuradas. Asegurate de definirlas en la configuración del servicio de Coolify antes de iniciar el stack.

Acceso a Grafana

Una vez logueado, los dashboards pre-configurados están disponibles en:

https://grafana-domain-you-have-set-in-coolify/dashboards

Vas a encontrar:

  • JVM Micrometer: Internals de JVM (pools de memoria, garbage collection, threads)
  • Spring Boot Observability: Métricas HTTP, tiempos de respuesta, tasas de error
Spring Boot Observability B4064b47d504cf799e722421c3b48a8a