v0.1 — ingesta y persistencia · 283 tests · clippy limpio

Un log suelto
no dice nada.
El patrón está en la correlación.

Las líneas de Nazca son invisibles a nivel del suelo y solo revelan su forma desde arriba. Nazca hace lo mismo con la integración clínica: una traza no es un span HTTP, es un mensaje HL7v2 que entró por un canal Mirth, gatilló un procedimiento almacenado en la base de datos y salió por una API. Nazca los une en un solo árbol sin modificar a ningún emisor, y disocia los datos de paciente antes de que toquen el almacenamiento.

50.000
eventos verificados celda por celda en el Parquet
0
identificadores directos persistidos
36
columnas · una sola tabla para logs, spans y métricas
283
tests · cargo deny limpio en las cuatro comprobaciones

Por qué Nazca

Lo que Datadog no cubre en un hospital.

01

Correlación HL7 sin tocar al emisor

Un mensaje HL7v2 no lleva traceparent, y un HIS certificado no se instrumenta en una tarde. Nazca deriva el trace_id del MSH-10, que ya viaja dentro del mensaje. Cada participante calcula la misma función sobre el mismo dato y llega al mismo identificador, sin coordinación.

02

La disociación va antes, no después

El destino es un object store WORM: lo escrito no se borra. Disociar después de escribir no es una versión peor de disociar antes, es no disociar. Por eso el motor no es una etapa del pipeline, es la condición de entrada al almacenamiento.

03

Una sola tabla, no tres

Logs, spans y métricas comparten esquema columnar. Tres tablas separadas obligan a un join entre almacenes para responder lo que de verdad importa: este ADT que falló, qué hizo la base de datos en ese mismo milisegundo.

04

Multi-empresa por prefijo, no por WHERE

La organización es el primer segmento de la ruta en el object store y una columna no nula del esquema. Un fichero Parquet nunca contiene filas de dos empresas: el aislamiento no depende de que una consulta filtre bien, sino de que el dato nunca se mezcló.

05

El recorrido completo no existe

Toda consulta lleva ventana temporal. Sin last explícito se asume last 1h, y la ventana no es opcional en el plan: el full scan no está prohibido, es que no se puede escribir.

06

Sin inyección posible por construcción

El traductor MQL es un parser real sobre la gramática, no concatenación de cadenas. Todo nombre de campo se resuelve contra el esquema: un identificador que no esté ahí no produce un campo, y sin campo no hay predicado. El texto del usuario nunca forma parte de una sentencia.

07

El árbol dice qué dedujo y qué observó

Un enlace derivado del MSH-10 es una inferencia de Nazca, no algo que el emisor declaró. Cada nodo de la traza lo marca. Quien investiga un incidente a las tres de la mañana tiene derecho a saber qué parte del árbol se vio y cuál se dedujo.

08

El motor de base de datos da igual

Las columnas db_instance, db_namespace y db_statement_id son neutrales: en Oracle llevan el SID y el SQL_ID —cruzable contra AWR—, en PostgreSQL la base y el queryid de pg_stat_statements, y en SQL Server la instancia y el query_hash. La consulta se escribe igual en los tres.

09

El APM lo pone Cóndor

El ingress del stack ya observa cada petición HTTP: latencia, upstream, estado, reintentos. Exporta por OTLP y Nazca lo ingiere como a cualquier emisor, de modo que el span del proxy y el del canal HL7 caen en el mismo árbol. Nazca no reimplementa APM: lo correlaciona con lo clínico, que es lo que nadie más hace.

10

Soberano, en su propia infraestructura

Rust, sin control plane, sin servicio gestionado. El Parquet vive en OxideStore, las consultas las resuelve OxideDB y el tráfico entra por Cóndor. Los datos clínicos no salen del perímetro, que es la única forma de que la residencia de datos no sea una promesa.

El diferenciador

Tres emisores, ninguna propagación, una sola traza.

El MSH-10 ya viaja dentro del mensaje: todos los participantes lo conocen sin que nadie se lo tenga que propagar. Si cada uno calcula sha256("hl7:" + MSH-10), los tres llegan al mismo trace_id sin coordinarse y sin que nadie modifique al emisor.

Canal Mirth
Recibe ADT^A31 del HIS. No habla OTLP; hace un POST con el mensaje crudo.
trace_id derivado476d4682b65101dea81d206cbc0fb04e
Base de datos
El procedimiento almacenado solo conoce el MSH-10. Reporta el identificador de su sentencia para cruzarla con el plan.
trace_id derivado476d4682b65101dea81d206cbc0fb04e
API destino
Ya exporta OTLP. Le basta un atributo hl7.control_id para entrar en la misma traza.
trace_id derivado476d4682b65101dea81d206cbc0fb04e
Cóndor · ingress
El proxy que ve entrar y salir la petición. Aporta latencia, upstream y estado de cada salto, y exporta por OTLP como cualquier otro emisor.
trace_id propagado476d4682b65101dea81d206cbc0fb04e
# Un canal de integracion manda el mensaje crudo. Nazca hace el resto.
POST /v1/events
x-nazca-org: hospital-central
x-nazca-project: his-prod

{ "signal": "span",
  "mirth_channel": "ADT_ENTRADA",
  "hl7_message": "MSH|^~\&|HIS_TEST|...|ADT^A31|MSG00001|T|2.5\rPID|1||11111111^^^HIS^MR||..." }

# Extrae MSH-3, MSH-5, MSH-9, MSH-10 y MSH-7; seudonimiza el PID-3;
# deriva el trace_id; y descarta el mensaje crudo. Nunca se almacena.

El estándar exige que el MSH-10 sea único por aplicación emisora, pero hay emisores que reinician su contador. Dos episodios lejanos en el tiempo pueden compartir trace_id; la ventana temporal obligatoria de toda consulta los mantiene separados en la práctica.

Disociación

El dato persistido ya no es dato personal.

Ningún identificador directo de paciente se escribe jamás en el almacenamiento. La seudonimización ocurre dentro del receptor, antes del buffer y antes del Parquet: px_ + 24 hex de HMAC-SHA256(llave, dominio ‖ 0x1F ‖ valor), con la llave fuera del almacén. Estable —el mismo paciente da siempre el mismo pseudónimo, que es lo que permite seguir un episodio— y no reversible sin ella.

Lo que entra

MSH|^~\&|HIS_TEST|...|ADT^A31|MSG00001|T|2.5
EVN|A31|20260823143705
PID|1||11111111^^^HIS_TEST^MR||PACIENTE^DE^PRUEBA
   ||19800101|M|||CALLE FALSA 123^^SANTIAGO
   ||+56912345678
PV1|1|O|CONSULTA_TEST^^^HOSPITAL_TEST

Lo que se persiste

MSH|^~\&|HIS_TEST|...|ADT^A31|MSG00001|T|2.5
EVN|A31|20260823143705
PID|[segmento-disociado]|px_8d7002fd5560ee4380519ea4
PV1|1|O|CONSULTA_TEST^^^HOSPITAL_TEST

# Lo técnico sobrevive: es lo que permite investigar.
# Lo clínico y lo personal, no.

El barrido cubre RUT en sus tres formas —con puntos, con guion y desnudo, este último validando el dígito verificador para no convertir cada identificador interno de nueve dígitos en un pseudónimo—, correo, teléfono, segmentos PID| completos y claves estructuradas anidadas, incluidas las que llevan prefijo (x-patient-id, http.request.body.email). Con service_env=prod y sin llave, el proceso no arranca.

MQL

Se lee de un vistazo o no sirve.

# Filtro por servicio y severidad
service == "api-admisiones" and severity >= error | last 1h

# Errores de ADT agrupados por canal Mirth
hl7_message_type == "ADT^A31" and status == "ERROR" | last 24h | count by channel

# Percentil 99 de latencia por span
duration_ns > 5e9 | last 7d | percentile(duration_ns, 99) by span_name

# La traza completa, en cascada
trace: 3f9a2b1c4d5e6f708192a3b4c5d6e7f8

# La historia de un pseudónimo — exige rol elevado
patient: px_a1b2c3d4e5f60718293a4b5c

org_id y project_id no se pueden filtrar desde MQL. No es que fuera peligroso —la sesión acota igualmente los prefijos que se leen— sino que daría la falsa impresión de que se eligen: escribir org_id == "otra-empresa" y recibir cero resultados haría creer que esa empresa no tiene datos.

Comparativa

Nazca convive con Datadog, no lo reemplaza.

CapacidadDatadogElasticSplunkNazca
APM y trazas de aplicación~ parcial~ parcial✓ vía Cóndor
Ingress con trazas propias~ integración✓ Cóndor, mismo stack
Infraestructura y hosting~ conviven
Parseo de HL7v2~ grok a mano~ regex a mano✓ nativo
Traza derivada del MSH-10✓ sin tocar al emisor
Correlación canal ↔ base de datos ↔ API~ join manual~ join manual✓ un solo árbol
Cruce con el plan de la sentencia✓ columna propia
Disociación PHI en el receptor~ scrubbing parcial~ ingest pipeline~ SEDCMD✓ antes del almacén
Pseudónimo estable y correlacionable✗ enmascara✗ enmascara✗ enmascara✓ HMAC por dominio
Vista de canal de integración✓ volumen, error, atascos
Datos fuera del perímetro✗ SaaS~ según despliegue~ según despliegue✓ nunca salen
Almacenamiento WORM verificable~ frozen✓ Parquet en object store
Coste por volumen de logs✗ por GB ingerido~ por nodo✗ por GB ingerido✓ el de tu object store

Comparación de alcance, no de madurez ni de benchmarks. El APM y las trazas de aplicación las cubre Cóndor, el ingress del mismo stack, que exporta por OTLP a Nazca: no es una carencia, es otra pieza. Donde Datadog sigue siendo mejor —infraestructura y hosting— conviven sin disputarse nada.

Estado

Qué funciona hoy.

M0Andamiaje, contratos públicos, CI y barrera de licenciascompleto
M1Ingesta OTLP y nativa, disociación PHI, parser HL7v2, Parquet particionado, multi-empresacompleto
M2Pelícano: traductor MQL y reconstrucción de trazas listos; falta el cliente de OxideDBparcial
M3Mirador: consola de operación y vista de canal Mirthpendiente
M4Garza: monitores, agentes y memoria de incidentespendiente

Los componentes llevan nombre de geoglifo y el nombre describe la función: Colibrí recibe, Glifo graba, Pelícano consulta, Araña teje el grafo de spans, Garza espera inmóvil y ataca cuando algo se mueve, y Mirador es donde se mira. El motor de consulta se llamó Cóndor hasta que quedó claro que ese nombre ya estaba tomado por el ingress del stack.