Decisiones de arquitectura

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.md pasa 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.