Módulo 3: Structured Logging For Pipelines
Presentación del módulo: de print() a un registro que una máquina puede leer
Por qué existe este módulo
En el módulo 1 convertiste kiosko_report.py en kiosko_pipeline, un paquete instalable con uv. En el módulo 2 le agregaste una suite de diecinueve tests con pytest, que confirma, en menos de un segundo, que extract_orders(), validate_orders() y transform_fact_orders() siguen calculando exactamente lo que deben calcular. Los dos módulos, juntos, responden una pregunta completa: ¿el código del pipeline es correcto? Sí — y ahora tienes una suite que lo confirma cada vez que algo cambia, en vez de tu propia memoria comparando números a mano.
Pero hay una segunda pregunta, distinta, que ningún test contesta: ¿qué pasó exactamente la última vez que este pipeline corrió de verdad? No en un test —donde tú controlas los datos, y sabes de antemano qué esperar—, sino en una corrida real, un martes a las tres de la mañana, sin que nadie esté mirando la terminal en ese momento. Si esa corrida falla, o si termina "bien" pero con un número que no cuadra, lo único que tienes para reconstruir qué pasó es lo que print() decidió mostrar. Y ya conoces, del módulo 1, exactamente qué muestra print() en kiosko_pipeline hoy: nueve líneas, al final, después de que las siete particiones de la semana ya terminaron de procesarse — nada mientras el pipeline corre, nada por partición, nada si algo se rompe antes de llegar a esa última línea.
Conexión con el módulo. Este módulo cierra exactamente esa brecha. Vas a reemplazar cada print() de kiosko_pipeline por eventos estructurados —objetos JSON, uno por línea— que cargan el contexto que hoy falta por completo: en qué corrida ocurrió esto (run_id), sobre qué partición de datos (partition_date), y qué pasó exactamente en cada paso —extracción, calidad, transformación, carga— de cada uno de los siete días. Al terminar este módulo, "qué pasó en la última corrida" deja de ser una pregunta que solo puede contestar quien estuvo mirando la terminal en el momento exacto, y se convierte en algo que cualquiera puede leer después, línea por línea, en un formato que tanto una persona como un programa pueden interpretar sin ambigüedad.
El caso que nos acompaña: los mismos datos, la misma semana, una forma nueva de contarla
Kiosko no cambia en este módulo — ni una fila nueva, ni una tienda nueva, ni un producto nuevo. Sigues trabajando exactamente con la semana fija que ya conoces de memoria: siete archivos, orders_2026-08-03.csv a orders_2026-08-09.csv, cuarenta órdenes en total, tres tiendas (S01 Kiosko Centro, S02 Kiosko Norte, S03 Kiosko Sur) y cuatro productos (P001 a P004), con un revenue total de 106.15 que ya viste reproducirse, idéntico, en el módulo 1 y en el módulo 2. A diferencia del módulo 2 —que agregó un octavo archivo, orders_2026-08-11.csv, como dato de prueba—, este módulo no agrega ningún archivo de datos nuevo. Lo único que cambia es cómo se cuenta la historia de una corrida: en vez de nueve líneas de texto libre al final, un registro estructurado, evento por evento, desde el primer archivo que se lee hasta el último que se carga.
Una analogía: la caja negra de un avión, no un diario íntimo
Un diario íntimo se escribe para uno mismo, en el momento, con el tono y el orden que a esa persona le parezcan naturales ese día. Nadie más necesita poder leerlo con precisión — si una entrada dice "hoy fue un día raro con las ventas", esa frase le sirve a quien la escribió, pero no le sirve a nadie más que quiera reconstruir, con exactitud, qué pasó. print() es exactamente ese diario íntimo: texto libre, pensado para que la persona que está mirando la terminal en ese instante entienda el contexto — pero inútil para cualquiera que llegue después, o para cualquier programa que intente procesarlo automáticamente.
La caja negra de un avión graba algo completamente distinto: no prosa, sino parámetros exactos, con marca de tiempo, en un formato estándar que un investigador —o un sistema automático— puede leer sin ambigüedad, mucho después del vuelo, sin haber estado presente. Nadie diseña una caja negra para que "se lea bien" en el momento; la diseñan para que cualquiera, después, con las herramientas correctas, pueda reconstruir exactamente qué pasó, en qué orden, con qué valores. Ese es, con precisión, el objetivo de este módulo: convertir cada corrida de kiosko_pipeline en algo tan reconstruible como la grabación de una caja negra, no tan efímero como una entrada de diario que solo tiene sentido para quien la escribió, el mismo día que la escribió.
Ejemplo trabajado: confirmando dónde quedó el pipeline al cerrar el módulo 2
Antes de tocar una sola línea de código, vale la pena confirmar, una última vez, el estado exacto en el que el módulo 2 dejó a kiosko_pipeline — porque ese estado es, literal, el punto de partida de este módulo. Desde la raíz de tu proyecto:
uv run python -m kiosko_pipeline
Qué esperar:
=== kiosko_pipeline: running the full week ===
rows_extracted=40 rows_valid=40 rows_rejected=0 rows_loaded=40
=== Revenue by store ===
S01 Kiosko Centro : revenue=38.3
S02 Kiosko Norte : revenue=38.8
S03 Kiosko Sur : revenue=29.05
=== Revenue by product ===
P001 Bottled Water 600ml : revenue=33.55
P002 Energy Bar : revenue=21.6
P003 Instant Coffee Sachet : revenue=10.5
P004 Phone Charger Cable : revenue=40.5
Total week revenue: 106.15
Estas nueve líneas son, con precisión, todo lo que kiosko_pipeline te dice hoy sobre una corrida completa. Fíjate en lo que no te dice: no dice cuándo corrió (ninguna marca de tiempo), no dice cuánto tardó cada partición, no dice en qué orden se procesaron los siete días, no dice si alguno tuvo más filas rechazadas que otro, y —el problema más serio, que la lección 2 va a demostrar en vivo— no dice absolutamente nada mientras el pipeline corre. Si algo se rompe en el día cuatro de siete, nunca vas a ver ninguna de estas nueve líneas — el print() que las genera vive todo junto, al final de main(), después de que run_pipeline() ya terminó de procesar las siete particiones.
Ahora, la carpeta que vas a modificar en este módulo, junto a la que ya existe:
kiosko_pipeline/
├── pyproject.toml (modulo 1, mas structlog en la leccion 5)
├── uv.lock (modulo 1-2, mas structlog en la leccion 5)
├── data/ (sin cambios -- mismos siete archivos)
├── src/
│ └── kiosko_pipeline/
│ ├── __init__.py (leccion 6: main() sin ningun print)
│ ├── __main__.py (sin cambios)
│ ├── extract.py (sin cambios)
│ ├── quality.py (sin cambios)
│ ├── transform.py (sin cambios)
│ ├── load.py (sin cambios)
│ ├── pipeline.py (leccion 6: un evento por paso, por dia)
│ └── logging_config.py (nuevo de este modulo -- leccion 6)
└── tests/ (modulo 2, sin cambios en este modulo)
Un solo archivo nuevo dentro del paquete, logging_config.py, y dos archivos existentes —pipeline.py y __init__.py— que ganan líneas de instrumentación, sin que ninguna fórmula de negocio cambie. Ningún archivo del módulo 1 (extract.py, quality.py, transform.py, load.py) se toca en absoluto: la lógica que ya tienes, probada por diecinueve tests, queda exactamente como está.
Diagrama: qué agrega este módulo, y qué no toca
flowchart TD
subgraph Existente["Modulos 1-2 -- sin cambios"]
A["extract.py, quality.py,\ntransform.py, load.py"]
B["tests/ -- 19 tests"]
end
subgraph Nuevo["Modulo 3 -- instrumentacion"]
C["logging_config.py\n(nuevo, leccion 6)"]
D["pipeline.py\n(gana eventos por paso)"]
E["__init__.py\n(main() sin ningun print)"]
end
A --> D
C --> D
C --> E
D --> F["uv run python -m kiosko_pipeline\n(JSON, una linea por evento)"]
E --> F
B -.->|"sigue vigilando A,\nsin tocar C-D-E"| A
La flecha punteada es intencional: la suite de tests del módulo 2 sigue vigilando extract.py, quality.py y transform.py exactamente igual que antes — ningún test de esos archivos necesita cambiar, porque ninguna línea de negocio de esos archivos cambia en este módulo. Lo que se instrumenta es la orquestación (pipeline.py) y el punto de entrada (__init__.py), no el cálculo en sí.
El mapa de este módulo
Leccion Pregunta que contesta
──────── ──────────────────────────────────────────────────────
L1 (esta) Que reemplaza print(), y por que hace falta
L2 Por que print() no escala, con un fallo real, en vivo
L3 Como funciona logging, el modulo de la libreria estandar
L4 Que es un log en JSON, y por que una maquina lo puede leer
L5 Como agregar contexto (run_id, partition_date) con structlog
L6 Logging_config.py conectado al pipeline completo, de punta a punta
L7 Que se loguea y que nunca se loguea
L8 Proyecto: el registro estructurado de una corrida completa
Las lecciones 2, 3 y 4 construyen, en ese orden, el problema y las dos piezas conceptuales que lo resuelven: primero ves el problema real de print() (lección 2), después el módulo logging de la librería estándar —niveles, handlers— sin JSON todavía (lección 3), y después por qué el formato importa tanto como el mecanismo: JSON en vez de texto libre (lección 4), armado a mano con lo que ya sabes de logging. La lección 5 introduce structlog, la librería que resuelve el problema que queda después de la lección 4: cómo llevar contexto —run_id, partition_date— sin tener que pasarlo como argumento a cada función. La lección 6 es el cuerpo ejecutable del módulo: logging_config.py completo, conectado a pipeline.py y __init__.py, corriendo la semana completa con salida JSON real. La lección 7 da un paso atrás, con criterio de seguridad: qué es seguro loguear y qué nunca lo es. Y la lección 8 —el mini-proyecto— junta todo y verifica que el resultado sea reproducible.
La frontera: qué NO cubre este módulo
Este módulo termina en el momento exacto en que un evento JSON sale por stdout. Lo que pase después de ese momento —enviar esos eventos a un agregador centralizado como Datadog, ELK o CloudWatch Logs, indexarlos para búsqueda, construir alertas sobre patrones, o correlacionar logs entre múltiples servicios— es, con toda intención, trabajo de infraestructura de observabilidad, no de este módulo. Esa disciplina completa vive en monitoring-observability-guide, una guía vinculada de este ecosistema. Aquí, el criterio de éxito es más modesto y más fundamental: que el log en sí esté bien formado, tenga el contexto correcto, y no filtre nada que no debería — la base sin la cual ningún agregador, por sofisticado que sea, puede ayudarte.
Tampoco entra en este módulo: manejo de errores ni reintentos. Vas a ver, en la lección 2, un fallo real del pipeline —y vas a loguear ese tipo de evento— pero decidir qué hacer cuando algo falla —reintentar, descartar, alertar— es, específicamente, el módulo 4 de esta guía (error-handling-and-retries), que empieza justo donde termina este.
Errores comunes
Asumir que "logging" y "print con más pasos" son lo mismo. Qué pasa: alguien, al escuchar "logging estructurado", imagina que el cambio es cosmético — cambiar print(...) por logging.info(...) y seguir escribiendo el mismo tipo de mensaje de texto libre, sin ningún cambio real de fondo. Por qué pasa: logging.info("algo pasó") se ve, en la superficie, casi idéntico a print("algo pasó") — la sintaxis es parecida, y es fácil no notar la diferencia real hasta ver los dos lado a lado. Cómo detectarlo: si al terminar este módulo tu código sigue interpolando todo en un solo string de texto —logging.info(f"processed {n} rows for {day}")— en vez de pasar campos separados —logging.info("day_processed", row_count=n, partition_date=day)—, no lograste la diferencia real que busca este módulo. Cómo corregirlo: el cambio de fondo no es la función que llamas, es la forma del dato que produces: un evento con campos nombrados, que un programa puede leer sin tener que adivinar dónde empieza y termina cada valor dentro de una oración en español o en inglés. Las lecciones 3 y 4 hacen esta distinción explícita, con código ejecutado de las dos formas, para que la diferencia quede clara con evidencia, no solo con una definición.
Pensar que este módulo es sobre "agregar más información" a la salida del pipeline. Qué pasa: alguien entiende el objetivo de este módulo como "imprimir más cosas" — más líneas, más detalle — sin conectar eso con la pregunta real que abrió esta lección: ¿puede alguien, después, reconstruir qué pasó? Por qué pasa: "más información" y "información reconstruible" se sienten parecidos, pero no lo son — puedes tener un print() extremadamente detallado y seguir sin poder responder, con certeza, en qué corrida específica ocurrió algo, si dos corridas del mismo día se mezclaron en la misma terminal. Cómo detectarlo: pregúntate si tu salida, tal como está, te permitiría distinguir los eventos de dos corridas distintas del pipeline si las dos escribieran a la misma terminal al mismo tiempo. Si la respuesta es no, agregar más texto no resuelve el problema real. Cómo corregirlo: el contexto que agrega la lección 5 —run_id, específicamente— existe para resolver exactamente este caso: cada evento, sin importar cuántos haya, queda etiquetado con la corrida exacta a la que pertenece, algo que ningún volumen de texto libre adicional puede lograr por sí solo.
Saltarse la lección 2 porque "ya sé que print() no es ideal para producción". Qué pasa: alguien con experiencia previa da por sabido el argumento contra print() en general, y salta directo a la sintaxis de logging sin ver, en el propio kiosko_pipeline, el fallo concreto que motiva este módulo. Por qué pasa: "print() no es para producción" es una regla que mucha gente ya escuchó, en abstracto, en algún curso o artículo — y da la sensación de que no hace falta volver a verla demostrada. Cómo detectarlo: si no puedes describir, con el pipeline de Kiosko como ejemplo concreto, qué información específica se pierde cuando el pipeline falla a mitad de la semana, la motivación de este módulo sigue siendo teórica para ti, no evidencia propia. Cómo corregirlo: la lección 2 no repite la regla general — rompe el pipeline real, a propósito, en un punto específico de la semana, y te muestra, literal, exactamente qué información sí y qué información no queda disponible después. Vale la pena verlo ejecutado, incluso si ya conoces el argumento en teoría.
Ejercicios
Ejercicio 1 — Confirma tu punto de partida. Desde la raíz de tu proyecto kiosko_pipeline/, corre uv run python -m kiosko_pipeline y confirma que obtienes las mismas nueve líneas de esta lección, terminando en Total week revenue: 106.15. Después, cuenta cuántas de esas nueve líneas mencionan explícitamente una marca de tiempo, un identificador de corrida, o el nombre de la partición de datos que se está procesando.
Ver solución
Si tu proyecto quedó exactamente como lo dejó el módulo 2, deberías ver las mismas nueve líneas, con Total week revenue: 106.15 al final. Contando con cuidado: cero de esas nueve líneas menciona una marca de tiempo, cero menciona un identificador de corrida, y cero menciona explícitamente partition_date como campo — el desglose por tienda y por producto usa store_id/product_id como parte del texto, pero no hay ningún campo estructurado que un programa pueda extraer sin parsear la oración completa. Ese conteo en cero es, con precisión, el punto de partida que este módulo cierra.
Ejercicio 2 — Predice el fallo silencioso. Sin ejecutar nada todavía (la lección 2 lo hace por ti), predice: si transform_fact_orders() lanzara una excepción mientras procesa el cuarto de los siete días de la semana, ¿cuántas de las nueve líneas de main() alcanzarías a ver en la terminal antes de que apareciera el error?
Ver solución
Cero, o como mucho una. Mirando el código de __init__.py tal como quedó al cerrar el módulo 2, los nueve print() viven todos después de la línea result = run_pipeline("data", WEEK_DAYS) — salvo, quizás, el primer print("=== kiosko_pipeline: running the full week ===\n"), que corre antes de esa línea. Si run_pipeline() lanza una excepción en cualquier punto de su ejecución —incluido el día 4 de 7—, ninguna de las ocho líneas restantes llega a ejecutarse nunca, porque Python interrumpe la función main() en el punto exacto donde ocurrió el error, sin llegar al resto del código que sigue. La lección 2 confirma esta predicción con un fallo real, ejecutado.
Ejercicio 3 — Compara con tus propias palabras. En 2-3 frases, explica la diferencia entre "un log que alguien puede leer en el momento" y "un log que alguien puede reconstruir después", usando la analogía de la caja negra de esta lección.
Ver solución
Un log que alguien puede leer en el momento —como el print() actual de kiosko_pipeline— depende de que una persona esté mirando la terminal exactamente cuando el evento ocurre, y de que esa persona recuerde, sin ayuda externa, el contexto necesario para interpretarlo. Un log que alguien puede reconstruir después no depende de ningún testigo presente: lleva, en sí mismo, todo el contexto necesario —cuándo ocurrió, en qué corrida, sobre qué partición de datos— para que cualquiera, con las herramientas correctas, entienda exactamente qué pasó, sin haber estado ahí. La caja negra de un avión existe, precisamente, porque casi nunca hay nadie mirando en el momento exacto del incidente — y un pipeline de datos que corre de madrugada, sin supervisión humana directa, está exactamente en esa misma situación.
Resumen y siguiente paso
En esta lección viste el punto de partida exacto de este módulo: kiosko_pipeline, tal como lo dejó el módulo 2, sigue reportando su resultado con nueve líneas de print(), todas al final de la corrida, sin marca de tiempo, sin identificador de corrida, y sin ninguna visibilidad de lo que pasa mientras el pipeline procesa cada una de las siete particiones de la semana. Confirmaste ese estado ejecutándolo una vez más, y recorriste el mapa completo de las ocho lecciones que convierten esas nueve líneas de texto en un registro estructurado, evento por evento, que tanto una persona como un programa pueden leer sin ambigüedad.
Antes de avanzar deberías poder: describir, con tus propias palabras, la diferencia entre un log pensado para leerse en el momento y uno pensado para reconstruirse después; nombrar qué información falta hoy en la salida de kiosko_pipeline (marca de tiempo, identificador de corrida, contexto de partición); y anticipar, a grandes rasgos, qué pasaría si el pipeline fallara a mitad de semana con el código actual.
La lección 2 deja de ser hipotética: rompe el pipeline real, a propósito, en el día cuatro de siete, y te muestra, literal, exactamente qué información queda disponible en la terminal —y cuál desaparece para siempre— con el código de print() que tienes hoy.
Recursos
- Python — documentación oficial del módulo
loggingde la librería estándar, la base de todo este módulo a partir de la lección 3. docs.python.org/3/library/logging.html. En inglés. - structlog — documentación oficial, la librería que este módulo introduce en la lección 5 para agregar contexto sin repetirlo en cada llamada. www.structlog.org/en/stable. En inglés.
- DISEÑO de
python-for-data-engineering-guide— el mapa completo de los ocho módulos de esta guía, incluida la frontera conmonitoring-observability-guide.src/guides/python-for-data-engineering-guide/DISENO.md. En español.