Telemetría en aplicaciones Spring Boot
La observabilidad permite entender qué sucede en una aplicación en ejecución mediante logs, métricas y trazas. En Spring Boot, Micrometer instrumenta métricas y observaciones; Micrometer Tracing conecta las observaciones con un trazador, y OpenTelemetry puede transportar los datos a un backend mediante OTLP. OTLP es un protocolo de exportación, no una interfaz de consulta ni un servidor que haya que instalar dentro de la aplicación. Véanse la guía de observabilidad de Spring Boot y el artículo de Spring sobre OpenTelemetry.
| Señal | Pregunta que responde | Ejemplo en Spring Boot |
|---|---|---|
| Logs | ¿Qué evento o error ocurrió? | Mensaje de SLF4J con excepción y traceId para correlación. |
| Métricas | ¿Cuánto ocurre y cómo evoluciona? | Tasa y duración de http.server.requests, memoria JVM, conexiones del pool. |
| Trazas | ¿Por dónde pasó una solicitud y dónde tardó? | Span de una petición HTTP y de las operaciones observadas que ejecutó. |
Una traza agrupa spans relacionados mediante un traceId; cada span tiene un spanId y puede apuntar a un span padre. W3C Trace Context propaga ese contexto entre servicios con el encabezado traceparent. La propagación entre procesos y la exportación a OTLP son pasos distintos: el primero relaciona solicitudes; el segundo envía datos al backend. Un span de SQL, por ejemplo, requiere instrumentación de JDBC adicional; la presencia de JPA o jOOQ no implica que cada consulta se registre automáticamente como span. Fuentes: trazas de Spring Boot, observabilidad de Spring Boot.
Una traza es un span raíz con sus descendientes. Cada span guarda su propia duración, y es esa jerarquía la que permite señalar dónde se fue la latencia:
flowchart TD
subgraph Proc1["⚙️ Proceso lis-app"]
direction TB
Raiz["span raíz · GET /api/v1/public/test<br/><b>spanId</b> a1b2 · <b>padre</b> ninguno · 120 ms"]
Proc["lis.procesar · observación propia<br/><b>spanId</b> c3d4 · <b>padre</b> a1b2 · 60 ms"]
Sql["consulta JDBC<br/><b>spanId</b> e5f6 · <b>padre</b> c3d4 · 25 ms<br/><i>solo con instrumentación aparte</i>"]
Cliente["llamada saliente con RestClient<br/><b>spanId</b> 0708 · <b>padre</b> a1b2 · 35 ms"]
end
subgraph Proc2["⚙️ Proceso servicio-remoto"]
direction TB
Remoto["GET /recurso<br/><b>spanId</b> 9a0b · <b>padre</b> 0708 · 30 ms"]
end
Raiz --> Proc
Proc --> Sql
Raiz --> Cliente
Cliente -->|"traceparent: 00-<b>traceId</b>-0708-01"| Remoto
classDef raiz fill:#D9ECFF,stroke:#165A91,stroke-width:3px,color:#102D46
classDef hijo fill:#DCF5E7,stroke:#247247,stroke-width:2px,color:#173C2A
classDef ausente fill:#F1F3F5,stroke:#50555A,stroke-width:2px,color:#24282B,stroke-dasharray:4 3
classDef remoto fill:#EAE1FF,stroke:#6941A5,stroke-width:2px,color:#30204F
class Raiz raiz
class Proc,Cliente hijo
class Sql ausente
class Remoto remoto
Los cinco spans comparten un mismo traceId; cada uno tiene su spanId y apunta al de su padre. El span remoto vive en otro proceso y se une a la traza porque recibió el encabezado traceparent, cuyo tercer campo es precisamente el spanId del emisor. El de JDBC aparece en gris punteado porque, como se indicó, no se genera sin instrumentación adicional. El archivo Mermaid está disponible aparte.
Qué usa este repositorio
Este proyecto LIS usa Spring Boot 4.1. En pom.xml ya están spring-boot-starter-opentelemetry y spring-boot-starter-actuator. El primero incluye el puente de Micrometer Tracing a OpenTelemetry y la exportación OTLP de métricas y trazas; Actuator aporta los endpoints de gestión. No se necesitan dependencias de Brave ni de Zipkin para la configuración actual. Fuente: Spring Boot, trazadores compatibles.
application.yaml configura spring.application.name: lis-app y estos destinos OTLP/HTTP:
| Señal | Propiedad de Spring Boot | Destino |
|---|---|---|
| Métricas | management.otlp.metrics.export.url |
${OTLP_ENDPOINT}/v1/metrics |
| Trazas | management.opentelemetry.tracing.export.otlp.endpoint |
${OTLP_ENDPOINT}/v1/traces |
| Logs | management.opentelemetry.logging.export.otlp.endpoint |
${OTLP_ENDPOINT}/v1/logs |
El host sale de la variable OTLP_ENDPOINT, con http://localhost:4318 como default de desarrollo. Es una variable y no un literal por la razón que se explica más abajo: localhost se interpreta desde el proceso Java.
Los prefijos no son homogéneos y conviene saber por qué: las métricas salen por management.otlp.metrics.* porque las exporta el registro de Micrometer (micrometer-registry-otlp), mientras que trazas y logs salen por management.opentelemetry.* porque los exporta el SDK de OpenTelemetry (opentelemetry-exporter-otlp). Ambas rutas llegan al mismo puerto 4318, pero son dos mecanismos distintos y sus propiedades no se intercambian.
Con spring.application.name Spring Boot deriva el atributo de recurso service.name, que es el nombre con el que la aplicación aparece en Grafana. Los demás atributos del recurso se configuran aparte, y tienen su propia sección.
Además de los destinos, el proyecto configura estos ajustes de observabilidad. Cada uno responde a una razón concreta, no a un valor por omisión que se dejó escrito:
| Ajuste | Valor | Por qué |
|---|---|---|
management.tracing.sampling.probability |
${OTEL_SAMPLING:1.0} |
Variable, no literal: 1.0 sirve en desarrollo y arruinaría producción. Muestreo |
management.opentelemetry.tracing.sampler |
parent-based-trace-id-ratio |
Explícito: una traza que entra muestreada no se parte. Muestreo |
...tracing.limits.max-attribute-value-length |
256 |
Si un atributo con PII se cuela, viaja truncado. Límites |
...logging.limits.max-attribute-value-length |
256 |
Lo mismo para los registros de log |
management.opentelemetry.resource-attributes |
deployment.environment |
Distinguir dev de producción en el backend |
...metrics.distribution.percentiles-histogram |
http.server.requests: true |
Sin histograma el SLO de p95 no es medible. Percentiles y SLO |
...metrics.distribution.slo |
100ms,300ms,500ms,1s |
Cubetas alineadas con el SLO declarado |
server.tomcat.mbeanregistry.enabled |
true |
Sin esto no hay métricas de hilos del contenedor |
Las convenciones de qué se instrumenta y cómo se nombra no están aquí, sino en ADR-0007: esto es configuración, aquello es criterio.
docker-compose.yaml incluye grafana/otel-lgtm, que recibe OTLP por el puerto 4318 y ofrece la interfaz de Grafana en http://localhost:3000. Si ese contenedor no está levantado, la aplicación arranca igual: el exportador registra errores de conexión periódicamente y las señales se pierden, sin afectar al tráfico HTTP. El Collector es útil para recibir, procesar y reenviar señales, pero no es obligatorio cuando el backend acepta OTLP directamente. localhost en estas propiedades se interpreta desde el proceso Java: si la aplicación se ejecuta dentro de otro contenedor, debe usarse un nombre de servicio o una dirección accesible desde ese contenedor. Fuente: artículo de Spring sobre OpenTelemetry.
Comunicación entre los componentes
Las flechas continuas muestran los caminos disponibles hoy. El appender OTEL conecta Logback con el exportador de logs. La consulta a /actuator/metrics ocurre dentro de la aplicación y es independiente del envío OTLP.
flowchart LR
Cliente["👤 Cliente HTTP"] -->|GET /api/v1/public/test| MVC
subgraph LIS["⚙️ LIS · Spring Boot 4.1"]
direction TB
MVC["🌐 Spring MVC y código de aplicación"] --> Observacion["🔎 Micrometer Observation"]
MVC --> SLF4J["📝 SLF4J y Logback"]
Observacion --> Registro["📊 Micrometer MeterRegistry"]
Observacion --> Puente["🧵 Micrometer Tracing y puente OTel"]
Registro -->|"métricas locales"| Actuator["🔍 Actuator /metrics"]
end
subgraph LGTM["📈 Grafana LGTM · Docker Compose"]
direction TB
Receptor["📥 Receptor OTLP/HTTP :4318"] --> Almacenes["🗄️ Almacenamiento de señales"]
Almacenes --> Grafana["📊 Grafana :3000"]
end
Registro -->|"OTLP /v1/metrics"| Receptor
Puente -->|"OTLP /v1/traces"| Receptor
SLF4J --> Consola["🖥️ Consola de la aplicación"]
SLF4J -->|"OTLP /v1/logs: appender OTEL"| Receptor
Operador["👩💻 Operador autenticado"] -->|"Consulta local"| Actuator
classDef actor fill:#FFF2CC,stroke:#7A5900,stroke-width:2px,color:#302300
classDef app fill:#D9ECFF,stroke:#165A91,stroke-width:2px,color:#102D46
classDef signal fill:#DCF5E7,stroke:#247247,stroke-width:2px,color:#173C2A
classDef backend fill:#EAE1FF,stroke:#6941A5,stroke-width:2px,color:#30204F
classDef local fill:#F1F3F5,stroke:#50555A,stroke-width:2px,color:#24282B
class Cliente,Operador actor
class MVC,Observacion app
class Registro,Puente,SLF4J signal
class Receptor,Almacenes,Grafana backend
class Actuator,Consola local
El archivo Mermaid del diagrama permite editarlo o renderizarlo fuera de la wiki.
Muestreo
El muestreo decide qué trazas se exportan. El valor predeterminado de Spring Boot es 0.1 (10 %) para no inundar el backend; este proyecto lo declara como variable con 1.0 de default de desarrollo:
management:
tracing:
sampling:
probability: ${OTEL_SAMPLING:1.0}
opentelemetry:
tracing:
sampler: parent-based-trace-id-ratio
Es una variable y no un literal a propósito: un 1.0 escrito en el archivo es una decisión de desarrollo que se despliega por olvido, y muestrear el 100 % del tráfico en producción cuesta dinero y ancho de banda sin mejorar el diagnóstico.
Con OpenTelemetry, además de la probabilidad se puede elegir el muestreador con management.opentelemetry.tracing.sampler:
| Muestreador | Comportamiento |
|---|---|
always-on |
Muestrea todas las trazas. |
always-off |
Descarta todas las trazas. |
trace-id-ratio |
Muestrea una fracción según management.tracing.sampling.probability, ignorando al padre. |
parent-based-always-on |
Sigue la decisión del padre; sin padre, muestrea todo. |
parent-based-always-off |
Sigue la decisión del padre; sin padre, descarta todo. |
parent-based-trace-id-ratio (predeterminado) |
Sigue la decisión del padre; sin padre, muestrea una fracción según la probabilidad. |
La familia parent-based es la que importa en un sistema con varios procesos: si la petición entra con un traceparent ya muestreado, el span hijo se muestrea también y la traza no queda partida por la mitad. Por eso está declarado explícitamente aunque sea el valor por omisión. Fuente: muestreo en Spring Boot.
Límites de atributos
OpenTelemetry permite acotar cuántos atributos lleva un span o un registro de log y cuánto puede medir el valor de cada uno. Este proyecto fija la longitud en las dos señales:
management:
opentelemetry:
tracing:
limits:
max-attribute-value-length: 256
logging:
limits:
max-attribute-value-length: 256
No es una optimización de tamaño, es una segunda línea de defensa. La regla 14 de 00-agents.md prohíbe PII en telemetría; si un atributo prohibido se cuela pese a la revisión, viaja truncado en lugar de completo. Las propiedades disponibles son max-attributes y max-attribute-value-length para ambas señales, y además max-events, max-links, max-attributes-per-event y max-attributes-per-link para trazas. Si hace falta control total, se registra un bean SpanLimits o LogLimits.
Atributos de recurso: quién emite
El recurso identifica al emisor en el backend. service.name lo deriva Spring Boot de spring.application.name, y el resto se añade con management.opentelemetry.resource-attributes:
management:
opentelemetry:
resource-attributes:
"[deployment.environment]": ${DEPLOY_ENV:dev}
Los corchetes son la forma documentada de conservar literal una clave con puntos dentro de un mapa. Estos atributos se combinan con las variables de entorno OTEL_RESOURCE_ATTRIBUTES y OTEL_SERVICE_NAME, y la propiedad gana sobre la variable. El registro OTLP de Micrometer no usa el bean Resource, pero sí respeta esta propiedad, así que sirve para las tres señales. Si se define un bean Resource propio, deja de aplicarse.
Etiquetas comunes y cardinalidad
Hay dos mecanismos, y no son el mismo:
| Propiedad | Alcance |
|---|---|
management.observations.key-values.* |
Etiquetas de baja cardinalidad en todas las observaciones (métricas y trazas). |
management.metrics.tags.* |
Etiquetas en todos los medidores de Micrometer, incluidos los que no vienen de una observación. |
La cardinalidad de una etiqueta es el número de valores distintos que puede tomar, y gobierna el coste: cada combinación de etiquetas es una serie temporal separada. Una etiqueta con el identificador de la orden o del paciente multiplica las series por el número de órdenes y, en este dominio, además expone información clínica. Las convenciones de nombres y cardinalidad de este proyecto están en ADR-0007; el resumen es que solo los conjuntos cerrados y pequeños van como baja cardinalidad, los identificadores técnicos van como alta cardinalidad y por tanto solo a la traza, y la PII no va en ninguna de las dos.
Logs: correlación y exportación
La aplicación usa SLF4J para emitir logs. Los niveles habituales son TRACE, DEBUG, INFO, WARN y ERROR; se pueden configurar con propiedades como logging.level.com.sineltek.lis=DEBUG. Spring Boot añade por defecto los identificadores de traza y span al patrón de logs cuando está activo Micrometer Tracing. Esto permite buscar en el backend de trazas la solicitud asociada con una línea de log. Fuente: correlación de logs en Spring Boot.
La URL OTLP de logs por sí sola no envía los mensajes de SLF4J. Spring Boot configura el exportador, pero necesita un appender para conectar Logback con él. Este proyecto ya incluye opentelemetry-logback-appender-1.0, lo declara en src/main/resources/logback-spring.xml y lo instala con OpenTelemetryAppenderInitializer. Por eso los mensajes se mantienen en consola y también se envían por OTLP a Grafana LGTM. La exportación de métricas y trazas es independiente del appender. Fuente: loggers en Spring Boot.
La dependencia del appender no forma parte de spring-boot-starter-opentelemetry ni la gestiona la BOM de Spring Boot. En pom.xml se fija la versión 2.28.1-alpha, cuya versión de instrumentación apunta al SDK OpenTelemetry 1.62.0 gestionado por Spring Boot 4.1.0. La versión se elige por el opentelemetry-api que declara, que debe coincidir con el SDK que gestiona Boot, no por ser la más reciente: 2.30.0-alpha pide el 1.64.0 y 2.31.1-alpha el 1.65.0. Al subir Spring Boot se comprueba primero qué SDK pasa a gestionar. La decisión y su motivo están en ADR-0006:
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-logback-appender-1.0</artifactId>
<version>${opentelemetry-instrumentation.version}</version>
</dependency>
src/main/resources/logback-spring.xml conserva el appender de consola y añade el de OpenTelemetry:
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<appender name="OTEL" class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="OTEL"/>
</root>
</configuration>
OpenTelemetryAppenderInitializer le proporciona el bean OpenTelemetry durante el arranque, porque Logback se configura antes que el contexto de Spring:
@Component
class OpenTelemetryAppenderInitializer implements InitializingBean {
private final OpenTelemetry openTelemetry;
OpenTelemetryAppenderInitializer(OpenTelemetry openTelemetry) {
this.openTelemetry = openTelemetry;
}
@Override
public void afterPropertiesSet() {
OpenTelemetryAppender.install(this.openTelemetry);
}
}
El appender no captura atributos adicionales del MDC: traceId y spanId viajan como campos propios del registro OpenTelemetry cuando hay un span activo. Los mensajes anteriores a la instalación se almacenan temporalmente hasta el límite del buffer del appender. En una aplicación clínica no se debe activar captureMdcAttributes=*, que podría exportar datos sensibles; si hacen falta otros atributos, se eligen por nombre y se revisa su contenido. Fuente: appender de Logback de OpenTelemetry.
Este cableado se rompe sin romper la compilación: quitar el <appender-ref ref="OTEL"/> o el @Component del inicializador deja el build en verde y la aplicación muda en el backend. ExportacionLogsOtelTest lo impide: arranca la aplicación, emite un log dentro de un span activo y afirma que llega al SDK con el mismo traceId y spanId, sustituyendo el exportador OTLP por uno en memoria para no depender de la red. Se comprobó rompiéndolo a propósito en esos dos puntos.
El endpoint /actuator/loggers permite consultar y cambiar niveles de log en ejecución mediante POST; no muestra ni exporta los mensajes. Este proyecto no lo expone: management.endpoints.web.exposure.include se limita a health,info. Subir un paquete a DEBUG desde fuera convertiría cualquier traza de depuración en una fuga de datos de paciente. Si se necesita para diagnosticar, se habilita de forma temporal y consciente, nunca de forma permanente. Con independencia de ello, evite registrar contraseñas, datos de pacientes, resultados clínicos o cuerpos completos de solicitudes.
Métricas: inspección y exportación
Actuator y Micrometer registran automáticamente métricas de JVM, proceso, servidor HTTP y otras integraciones presentes. Por ejemplo, después de llamar a un controlador MVC puede consultarse http.server.requests. Una métrica agrupa medidas por etiquetas; use etiquetas con pocos valores posibles, como método HTTP o estado. Identificadores de pacientes, órdenes o solicitudes producirían demasiadas series y podrían exponer información sensible. Fuente: métricas de Spring Boot.
Hay dos mecanismos diferentes:
| Mecanismo | Uso en este proyecto |
|---|---|
GET /actuator/metrics y GET /actuator/metrics/{nombre} |
Inspección puntual dentro de la aplicación. No está expuesto: la lista de endpoints web se limita a health,info. Para consultarlo hay que añadirlo explícitamente. |
OTLP a /v1/metrics |
Exportación periódica al backend configurado; no depende de consultar /actuator/metrics. |
Para obtener spans de cada consulta SQL, la instrumentación habitual es net.ttddyy.observation:datasource-micrometer-spring-boot, que envuelve el DataSource y crea una observación por consulta. No forma parte de Spring Boot. Conviene comprobar el resultado: combinada con el starter de OpenTelemetry se han reportado spans duplicados para una misma consulta, porque dos manejadores distintos atienden la misma observación. Fuente: datasource-micrometer.
/actuator/prometheus es otra opción, orientada al scraping por Prometheus. Requiere la dependencia micrometer-registry-prometheus y exponer ese endpoint; no está configurado aquí. No debe confundirse con la exportación OTLP ya presente. Fuente: métricas OTLP y Prometheus en Spring Boot.
Métricas que ya existen sin escribir código
Antes de instrumentar algo conviene saber qué se publica solo. Registrar un contador propio de peticiones o de errores es duplicar peor lo que ya está:
| Familia | Qué responde | Requiere |
|---|---|---|
jvm.* |
Memoria, pools, recolección de basura, hilos, clases cargadas, tiempo de JIT | — |
system.*, process.*, disk.* |
CPU, descriptores de fichero, tiempo en marcha, espacio libre | — |
application.started.time, application.ready.time |
Cuánto tardó en arrancar y en estar listo para atender | — |
http.server.requests |
Latencia, estado y resultado de cada petición MVC | — |
jdbc.connections.*, hikaricp.* |
Pool de conexiones: activas, inactivas, máximo, esperas | — |
logback.events |
Contador de eventos por nivel: la tasa de ERROR sin escribir una línea |
— |
tomcat.* |
Hilos ocupados, conexiones, sesiones del contenedor servlet | server.tomcat.mbeanregistry.enabled: true, ya activado |
executor.* |
Colas y pools de ThreadPoolTaskExecutor y ThreadPoolTaskScheduler |
Que existan esos beans |
ssl.chain.expiry |
Segundos hasta que caduca cada cadena de certificados | Que haya bundles SSL configurados |
| Hilos virtuales | Estadísticas de hilos virtuales | io.micrometer:micrometer-java21, no está en el classpath |
Las de pool y logback.events son las dos que más rápido explican una caída: una saturación del pool de Hikari y un pico de ERROR se ven antes aquí que en cualquier traza. Fuente: métricas soportadas en Spring Boot.
Percentiles, histogramas y el SLO
00-agents.md fija un SLO provisional de p95 < 500 ms en consultas operativas. Un percentil no se puede calcular desde un contador y una media, así que sin configurarlo ese SLO no es medible. Hay tres mecanismos y conviene no confundirlos:
| Propiedad | Qué produce |
|---|---|
management.metrics.distribution.percentiles-histogram |
Histograma agregable: el backend calcula el percentil, y puede hacerlo sobre varias instancias. |
management.metrics.distribution.percentiles |
Percentiles calculados dentro de la aplicación. No son agregables entre instancias. |
management.metrics.distribution.slo |
Histograma acumulado con cubetas en los límites que se declaren. |
Este proyecto configura el histograma y las cubetas del SLO para las peticiones HTTP:
management:
metrics:
distribution:
percentiles-histogram:
http.server.requests: true
slo:
http.server.requests: 100ms,300ms,500ms,1s
Con eso, «¿cumplimos el p95?» y «¿cuántas peticiones pasaron de 500 ms?» son consultas, no estimaciones. Si el número de cubetas resulta excesivo, minimum-expected-value y maximum-expected-value acotan el rango. Fuente: histogramas y percentiles en Micrometer.
Exemplars: de una métrica a su traza
Un exemplar es un puntero desde una muestra de una métrica hasta una traza concreta que la produjo. Es lo que convierte «el p95 se fue a 2 segundos a las 11:40» en «esta es una de las peticiones que tardó 2 segundos, y aquí está su traza».
OTLP los admite y no hay nada que configurar: requieren un bean ExemplarContextProvider, que Spring Boot autoconfigura cuando está Micrometer Tracing, y está. Por omisión solo se incluyen trazas muestreadas, lo que se controla con management.tracing.exemplars.include (sampled-traces es el valor por defecto). Fuente: OTLP en Spring Boot.
Trazas: instrumentación y propagación
Spring Boot crea observaciones para las solicitudes MVC y para los clientes HTTP construidos con sus builders configurados. Para mantener el contexto al llamar a otro servicio, inyecte RestClient.Builder, RestTemplateBuilder o WebClient.Builder; un cliente creado manualmente puede perder la instrumentación y el encabezado de propagación. Fuente: propagación de trazas en Spring Boot.
El siguiente ejemplo muestra la comunicación si LIS llama a otro servicio instrumentado; ese segundo servicio no forma parte de este repositorio. traceparent viaja en la petición HTTP, mientras cada aplicación exporta sus propios spans al backend.
sequenceDiagram
actor Cliente as 👤 Cliente
participant LIS as ⚙️ LIS / Spring Boot
participant HTTP as 🌐 Cliente HTTP con builder
participant Servicio as ⚙️ Otro servicio instrumentado
participant LGTM as 📈 Backend OTLP
Cliente->>LIS: GET /api/ejemplo
activate LIS
Note over LIS: Observación HTTP: crea o continúa traceId
LIS->>HTTP: Llamada mediante RestClient.Builder
HTTP->>Servicio: GET /recurso + header traceparent
activate Servicio
Note over Servicio: Extrae traceId y crea un span hijo
Servicio-->>HTTP: Respuesta
deactivate Servicio
HTTP-->>LIS: Respuesta
LIS-->>Cliente: Respuesta HTTP
deactivate LIS
LIS-)LGTM: Span de LIS por OTLP, si fue muestreado
Servicio-)LGTM: Span remoto por OTLP, si fue muestreado
También está disponible el archivo Mermaid de la secuencia.
Para medir una operación de negocio propia, use ObservationRegistry. Una observación puede producir una métrica y un span con el mismo nombre:
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.stereotype.Service;
@Service
class ProcesamientoService {
private final ObservationRegistry observations;
ProcesamientoService(ObservationRegistry observations) {
this.observations = observations;
}
void procesar() {
Observation.createNotStarted("lis.procesar", observations)
.lowCardinalityKeyValue("tipo", "rutina")
.observe(() -> {
// Operación que se desea medir.
});
}
}
Ese único bloque de código alimenta dos señales a la vez, y cada tipo de etiqueta llega a un sitio distinto:
flowchart LR
Codigo["🧩 Código de negocio<br/>Observation.createNotStarted(...)"] --> Obs["🔎 Observación<br/>lis.procesar"]
Obs --> Baja["🏷️ Etiquetas de baja cardinalidad<br/>tipo=rutina"]
Obs --> Alta["🏷️ Etiquetas de alta cardinalidad<br/>solo a la traza"]
Baja --> Metrica["📊 Métrica<br/>lis.procesar · contador y duración"]
Baja --> Span["🧵 Span<br/>lis.procesar"]
Alta --> Span
Metrica -->|"OTLP /v1/metrics"| Backend["📈 Backend OTLP"]
Span -->|"OTLP /v1/traces"| Backend
classDef codigo fill:#D9ECFF,stroke:#165A91,stroke-width:2px,color:#102D46
classDef obs fill:#DCF5E7,stroke:#247247,stroke-width:2px,color:#173C2A
classDef etiqueta fill:#FFF2CC,stroke:#7A5900,stroke-width:2px,color:#302300
classDef salida fill:#EAE1FF,stroke:#6941A5,stroke-width:2px,color:#30204F
class Codigo codigo
class Obs obs
class Baja,Alta etiqueta
class Metrica,Span,Backend salida
El archivo Mermaid de la observación permite reutilizarlo. Las etiquetas de baja cardinalidad se incorporan a métricas y trazas. Las de alta cardinalidad solo se añaden a las trazas, pero en una aplicación clínica también deben excluir datos sensibles. No es necesario crear un span para cada método; empiece por operaciones que ayuden a explicar latencia o fallos. Si necesita únicamente un span, use la API Tracer. Las anotaciones @Observed y @NewSpan requieren habilitar management.observations.annotations.enabled=true y añadir el soporte AspectJ indicado por Spring Boot; la API programática anterior no requiere esa configuración. Fuentes: observabilidad, spans personalizados.
Para @Async con el ejecutor autoconfigurado, Spring Boot 4.1 ofrece spring.task.execution.propagate-context=true. Si se crea un ejecutor propio, configure ContextPropagatingTaskDecorator. Sin propagación, los logs y spans de la tarea pueden perder el contexto de la solicitud. Fuente: propagación entre hilos.
Anotaciones de observación
Micrometer admite @Observed, @Timed, @Counted, @MeterTag y @NewSpan. No funcionan por defecto: hay que poner management.observations.annotations.enabled: true y añadir org.springframework.boot:spring-boot-starter-aspectj, que trae aspectjweaver. Ninguna de las dos cosas está en este proyecto, por decisión de ADR-0007.
La razón es concreta: anotar un método que ya está instrumentado —un controlador MVC, un repositorio de Spring Data— produce observaciones duplicadas, una de la instrumentación automática y otra de la anotación. Para evitarlo hay que desactivar la automática por propiedades o con un ObservationPredicate, y entonces la anotación deja de ser el atajo que parecía. La instrumentación de este proyecto es programática con ObservationRegistry.
Silenciar y desactivar señales
Instrumentar de más también es un problema: cuesta dinero en el backend y entierra lo que importa. Hay cuatro niveles, de más fino a más grueso:
| Mecanismo | Alcance |
|---|---|
management.observations.enable.<prefijo>: false |
Impide las observaciones cuyo nombre empieza por ese prefijo. |
Bean ObservationPredicate |
Control programático: una observación se reporta solo si todos los predicados devuelven true. |
management.metrics.enable.<prefijo>: false |
Filtra medidores por prefijo de identificador. |
management.opentelemetry.enabled: false |
Desactiva el SDK completo: métricas, trazas y logs pasan a implementaciones no-op. Equivale a OTEL_SDK_DISABLED invertido. Los propagadores de contexto siguen funcionando. |
Un caso habitual: management.observations.enable.spring.security: false corta las observaciones que genera Spring Security, que con muestreo al 100 % añaden bastante ruido a cada traza. Este proyecto todavía no las desactiva, pero cuando llegue la autorización por recurso de M8 conviene revisar si aportan o estorban.
Ojo con el nivel más grueso: Spring Boot no usa la parte de métricas de OpenTelemetry —las exporta Micrometer—, así que desactivar OpenTelemetry no apaga necesariamente las métricas.
Baggage
Baggage es un par clave-valor que viaja con la traza y cruza procesos. Se crea con la API Tracer de Micrometer, se propaga automáticamente con W3C (no con B3), y dos propiedades deciden hasta dónde llega:
| Propiedad | Efecto |
|---|---|
management.tracing.baggage.remote-fields |
Publica el campo en una cabecera HTTP, de modo que lo reciba el siguiente servicio. |
management.tracing.baggage.correlation.fields |
Copia el campo al MDC, de modo que aparezca en cada línea de log. |
En este proyecto no se usa baggage (ADR-0007). Leídas juntas, esas dos propiedades describen el camino más corto para que un dato clínico salga por una cabecera HTTP y termine escrito en los logs de otro sistema. Si alguna vez hace falta propagar algo, se decide en un ADR y no puede ser PII.
Configuración por variables de entorno
Spring Boot 4.1 traduce al arrancar un subconjunto de las variables OTEL_* del SDK a sus propias propiedades. Esto importa para desplegar: un contenedor se configura por entorno sin reescribir application.yaml. La traducción se desactiva con management.opentelemetry.map-environment-variables: false.
Dos reglas de precedencia que conviene tener claras:
- La variante específica de señal gana sobre la general y se usa tal cual:
OTEL_EXPORTER_OTLP_TRACES_ENDPOINTse toma literal. - Cuando se usa la general como respaldo, Spring Boot le añade la ruta de la señal:
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318producehttp://collector:4318/v1/tracespara trazas, y lo equivalente para métricas y logs.
Las más relevantes aquí:
| Variable | Propiedad |
|---|---|
OTEL_SDK_DISABLED |
management.opentelemetry.enabled (invertida) |
OTEL_SERVICE_NAME, OTEL_RESOURCE_ATTRIBUTES |
Atributos del recurso; las propiedades de resource-attributes ganan |
OTEL_EXPORTER_OTLP_ENDPOINT |
Destino de las tres señales, con /v1/... añadido |
OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG |
management.opentelemetry.tracing.sampler y la probabilidad |
OTEL_METRIC_EXPORT_INTERVAL |
management.otlp.metrics.export.step |
OTEL_EXPORTER_OTLP_HEADERS |
Cabeceras del exportador, para backends que exigen autenticación |
OTEL_RESOURCE_ATTRIBUTES es una lista clave=valor separada por comas, y todo carácter fuera del rango permitido va codificado en porcentaje: key3=spring%20boot.
Las demás variables del SDK no están soportadas. Para que valgan todas hay que aportar un bean OpenTelemetry propio con opentelemetry-sdk-extension-autoconfigure, y eso apaga la autoconfiguración de Spring Boot y puede romper la observabilidad integrada. No es el camino de este proyecto. Fuente: soporte de OpenTelemetry en Spring Boot.
Personalización avanzada
Cuando el destino exige algo que las propiedades no cubren —autenticación, TLS con un bundle concreto, cabeceras calculadas— se registran beans que se invocan antes de construir el exportador y tienen precedencia sobre la autoconfiguración:
| Señal | Beans |
|---|---|
| Trazas | OtlpHttpSpanExporterBuilderCustomizer, OtlpGrpcSpanExporterBuilderCustomizer |
| Logs | OtlpHttpLogRecordExporterBuilderCustomizer, OtlpGrpcLogRecordExporterBuilderCustomizer |
| Métricas | Bean OtlpMetricsSender |
Para las etiquetas de las peticiones HTTP, en lugar de tocar el exportador se extiende DefaultServerRequestObservationConvention (añadir etiquetas) o se implementa ServerRequestObservationConvention (reemplazarlas). Es el punto donde se decide qué describe una petición, y por tanto donde hay que releer la regla de cardinalidad antes de añadir nada.
Observabilidad en los tests
Los componentes de trazado que reportan datos no se autoconfiguran con @SpringBootTest. Es comportamiento de Spring Boot, y es la razón por la que la batería de tests no llena el backend ni ensucia la salida con errores de conexión al puerto 4318.
La exportación de logs no es un componente de trazado y no entra en esa exención. Por eso ExportacionLogsOtelTest desactiva explícitamente el exportador OTLP de logs y lo sustituye por un procesador en memoria:
@SpringBootTest(properties = "management.logging.export.otlp.enabled=false")
Sin esa propiedad, el test dependería de que hubiera un backend escuchando. Con ella, comprueba lo que de verdad es responsabilidad de este repositorio: que el log sale de SLF4J y llega al SDK con su contexto de traza.
Comprobación local
- Inicie los servicios con
make devy la aplicación conmake run(o usemake dev-full, que también migra y carga datos de demostración). Los valores de desarrollo deDB_HOST,DB_PORT,DB_NAME,DB_USERyDB_PASSya están configurados.grafana-lgtmdebe estar accesible desde el proceso Java enlocalhost:4318. - Reinicie la aplicación después de añadir el appender y llame varias veces a
GET http://localhost:8080/api/v1/public/test. Compruebe queTest endpoint calledaparece en consola con un identificador de traza. - Abra
http://localhost:3000y, en Drilldown → Logs, seleccione la fuente Loki, el serviciolis-appy un intervalo que incluya las peticiones nuevas. BusqueTest endpoint calledy compruebe que el registro contiene el mismotraceIdque la consola. En Traces, busque la misma traza; en Metrics, compruebe la serie HTTP después del siguiente intervalo de exportación (predeterminado1m). /actuator/metricsy/actuator/loggersno están expuestos:management.endpoints.web.exposure.includese limita ahealth,info. Solohealthes público;SecurityConfigexige autenticación para los demás endpoints que se expongan en el futuro. Si no aparece una señal en Grafana, revise el puerto 4318,OTLP_ENDPOINTy los errores del exportador en consola.
Resultado comprobado
El 25 de septiembre de 2026 se reinició la aplicación con el appender OTLP y se invocó GET /api/v1/public/test. El mensaje Test endpoint called apareció tanto en la consola como en Grafana → Drilldown → Logs (Loki), con el mismo identificador de traza y de span. En Loki estos campos se muestran como trace_id y span_id; en la consola forman el bloque [traceId-spanId]. Para repetir la búsqueda en Explore con la fuente Loki:
{service_name="lis-app"} |= "Test endpoint called"
Después de cambiar pom.xml, logback-spring.xml o la inicialización del appender hay que reiniciar el proceso Java. Una instancia que siga ejecutando la versión anterior continuará escribiendo solo en consola; los mensajes anteriores al reinicio no se envían retroactivamente a Loki.
Las trazas técnicas y los logs ayudan a diagnosticar la aplicación, pero no sustituyen la auditoría funcional de cambios en resultados, validaciones o informes clínicos.
Enfoques para otros proyectos Spring Boot
| Contexto | Punto de partida |
|---|---|
| Spring Boot 4.x, como este repositorio | org.springframework.boot:spring-boot-starter-opentelemetry y propiedades management.* para OTLP. |
| Spring Boot 3.x con integración Micrometer | Actuator, micrometer-tracing-bridge-otel y el exportador OTLP, según la versión; no copiar las propiedades de Boot 4 sin verificarlas. |
| Aplicación JVM que requiere instrumentación amplia sin cambiar código | Agente Java de OpenTelemetry, configurado con variables como OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT y OTEL_METRIC_EXPORT_INTERVAL, y con validación de compatibilidad. |
El siguiente árbol resume esa decisión y los dos añadidos que el starter no trae:
flowchart TD
Inicio(["¿Qué instrumentación uso?"]) --> Boot{"¿Versión de<br/>Spring Boot?"}
Boot -->|"4.x"| Cambio{"¿Puedo tocar<br/>el código y el pom?"}
Boot -->|"3.x"| Tres["Actuator +<br/>micrometer-tracing-bridge-otel<br/>+ exportador OTLP"]
Cambio -->|"Sí · caso de LIS"| Starter["spring-boot-starter-opentelemetry<br/>propiedades management.*"]
Cambio -->|"No"| Agente["Agente Java de OpenTelemetry<br/>variables OTEL_*"]
Starter --> Logs{"¿Necesito los logs<br/>en el backend?"}
Logs -->|"Sí"| Appender["Añadir appender de Logback<br/>e instalarlo con el bean OpenTelemetry"]
Logs -->|"No"| Listo(["Métricas y trazas ya salen por OTLP"])
Starter --> SQL{"¿Necesito spans<br/>de consultas SQL?"}
SQL -->|"Sí"| Jdbc["datasource-micrometer-spring-boot<br/>vigilar spans duplicados"]
SQL -->|"No"| Listo
Agente --> Aviso["No combinar con el starter:<br/>duplica señales"]
Tres --> Aviso2["No copiar propiedades de Boot 4<br/>sin verificarlas"]
classDef pregunta fill:#FFF2CC,stroke:#7A5900,stroke-width:2px,color:#302300
classDef opcion fill:#D9ECFF,stroke:#165A91,stroke-width:2px,color:#102D46
classDef extra fill:#DCF5E7,stroke:#247247,stroke-width:2px,color:#173C2A
classDef aviso fill:#FFE3E3,stroke:#C92A2A,stroke-width:2px,color:#5C0000
classDef fin fill:#EAE1FF,stroke:#6941A5,stroke-width:2px,color:#30204F
class Boot,Cambio,Logs,SQL pregunta
class Starter,Tres,Agente opcion
class Appender,Jdbc extra
class Aviso,Aviso2 aviso
class Inicio,Listo fin
También está el archivo Mermaid de la decisión.
El starter oficial de Spring Boot 4, el starter mantenido por la comunidad OpenTelemetry y el agente Java son caminos distintos. Al combinar instrumentaciones se pueden duplicar señales; elija una estrategia principal y compruebe qué produce cada componente. Para comparar enfoques, consulte el artículo de Spring, la guía de Baeldung para Boot 3, la guía de SigNoz sobre el agente, la explicación de Dan Vega para Boot 4 y la comparación de Uptrace. Para propiedades concretas de este proyecto, prevalece la referencia oficial de Spring Boot.