ADR-0007: Convenciones de observabilidad
Estado: Aceptado · Fecha: 2026-09-25
Contexto
Con ADR-0006 las tres señales llegan al backend. Lo que no existe es una convención de qué se instrumenta y cómo se nombra, y M1 tiene 22 slices por delante. La definición de terminado exige en cada uno «trazas y métricas en los puntos de decisión, sin PII», así que la pregunta se va a plantear 22 veces. Sin una decisión previa, lo que se hereda son 22 vocabularios distintos y una auditoría imposible.
El riesgo específico de este dominio no es la falta de datos, es el exceso. Una etiqueta con el
documento del paciente convierte el backend de telemetría en una base de datos clínica paralela,
sin consentimiento, sin control de acceso y sin auditoría. La regla 14 de 00-agents.md prohíbe
PII en logs; el problema es que un span y una métrica son igual de exportables que un log y la
regla no los nombraba.
Decisión
Las observaciones se crean con ObservationRegistry de Micrometer, no con la API de
OpenTelemetry. Es la recomendación explícita de Spring y una observación produce a la vez la
métrica y el span, con el mismo nombre y las mismas etiquetas de baja cardinalidad. Usar la API de
OpenTelemetry directamente produce un span sin su métrica y ata el código al SDK.
Nombres. lis.<modulo>.<operacion>, en minúsculas, con el vocabulario del dominio en español:
lis.admision.registrar_orden, lis.muestras.recibir_muestra, lis.informes.emitir_informe. El
módulo es el del mapa de PLAN_MAESTRO.md. Una operación de negocio, no un método.
Cardinalidad, que es donde se rompen las métricas y se filtra la PII:
| Tipo | Qué admite | Ejemplos |
|---|---|---|
lowCardinalityKeyValue |
Conjunto cerrado y pequeño de valores. Va a métricas y trazas | prioridad=urgente, resultado=rechazada, motivo=sin_alicuota |
highCardinalityKeyValue |
Identificadores técnicos internos. Va solo a trazas | orden_id=<uuid> |
| Ninguno de los dos | Nombre, documento, historia clínica, valores de resultado, diagnóstico | — |
Prohibido en telemetría, ampliando la regla 14 a las tres señales: ni en el mensaje de un log, ni en un atributo de span, ni en una etiqueta de métrica, ni en baggage. Los identificadores técnicos (UUID de orden, de muestra, de prueba) sí, porque no identifican a una persona sin acceso a la base. Ante la duda, no se emite.
Baggage desactivado. Baggage viaja por la red en una cabecera HTTP y puede volcarse al MDC con
management.tracing.baggage.correlation.fields, así que es el camino más corto para que un dato
clínico acabe en los logs de otro sistema. Si algún día hace falta propagar algo, se decide en un
ADR y no puede ser PII.
El dominio no loguea. Loguea la capa que orquesta —aplicacion, infraestructura, api—; el
dominio devuelve errores tipados y deja que quien decidió invocarlo decida qué registrar. Es la
misma razón por la que el dominio no conoce Spring: un Logger es una dependencia de
infraestructura disfrazada de utilidad, y un dominio que loguea acaba logueando el valor que acaba
de validar.
Sin anotaciones @Observed / @Timed. Requieren aspectjweaver, que no está en el
classpath, y sobre código ya instrumentado —un controlador MVC, un repositorio— producen
observaciones duplicadas. La instrumentación es programática y explícita.
Muestreo. management.tracing.sampling.probability es una variable con 1.0 como default de
desarrollo, no un literal: el default de Boot es 0.1 y muestrear el 100% en producción inunda el
backend. El sampler es parent-based-trace-id-ratio, declarado explícitamente: una petición que
llega con un traceparent ya muestreado se sigue hasta el final.
Límites de atributos. max-attribute-value-length: 256 en trazas y en logs, como segunda
línea de defensa: si un atributo prohibido se cuela, viaja truncado.
No se instrumenta lo que ya está instrumentado. Boot publica sin escribir una línea las
métricas de JVM, sistema, arranque, http.server.requests, hikaricp y jdbc.connections,
logback.events y —con el registro de MBeans activo— Tomcat. Un contador propio de peticiones o
de errores es duplicar http.server.requests y logback.events peor.
Cómo se verifica
ExportacionLogsOtelTest cubre el transporte y la correlación. La regla «el dominio no loguea» se
convierte en una regla de ArchUnit en S1.01, junto a las cinco que ya existen: una prohibición que
depende de la disciplina no es una regla, y este repositorio ya tomó esa decisión en
ADR-0004.
La ausencia de PII no es verificable por test: ningún test distingue un UUID de un número de documento. Es materia de revisión adversarial, y por eso está escrita aquí y en la constitución en lugar de confiarse a la buena memoria de quien instrumenta.
Consecuencias
- Los 22 slices heredan un vocabulario, no lo negocian cada uno.
- La pregunta «¿qué regla no tiene observabilidad?» se responde con una búsqueda por
lis.. - El SLO de
00-agents.mdpasa a ser medible: hay histograma y cubetas alineadas con él. − Instrumentar a mano es más código que una anotación. Se acepta a cambio de no duplicar observaciones y de no añadir AspectJ. − La regla de cardinalidad exige pensar en cada etiqueta. Es el punto del proceso donde se evita una fuga, así que el coste es el objetivo.
Lo que esta decisión NO resuelve
No fija el formato de consola. Los logs llevan traceId en el patrón de correlación y llegan a
Loki con trace_id y span_id como campos propios, pero la consola sigue siendo texto: activar
logging.structured.format.console es una decisión pendiente, y conviene tomarla junto con la del
perfil de producción, que todavía no existe.
No añade el identificador de correlación a la respuesta HTTP. Sigue siendo la deuda de S1.01.
No instrumenta nada todavía: no hay apenas código de negocio que instrumentar. La primera observación propia llega con el primer caso de uso real.
No decide sobre spans de SQL. datasource-micrometer-spring-boot los produce, y con el starter de
OpenTelemetry se han reportado spans duplicados. Se evalúa en S1.22, con los logs ya funcionando
para poder notar la duplicación.
Alternativas descartadas
API de OpenTelemetry directa. Produce spans sin métrica, ata el código de negocio al SDK y Spring recomienda explícitamente lo contrario.
Anotaciones sobre los casos de uso. Menos código, pero duplican observaciones sobre lo ya instrumentado y añaden AspectJ al classpath por comodidad de sintaxis.
Instrumentar por método, de forma exhaustiva. Produce trazas con decenas de spans donde ninguno explica nada y multiplica el coste del backend. Se instrumentan puntos de decisión.