Initial commit: SaveUrl project migration from Drive
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
# Documentación del proyecto `SaveUrl`
|
||||
|
||||
Esta carpeta contiene la documentación completa del proyecto (sin depender del `README.md`).
|
||||
|
||||
## Lectura rápida (por rol)
|
||||
|
||||
- **Operación / soporte**: empieza por `07_RUNBOOK_OPERACION.md` y `08_TROUBLESHOOTING.md`.
|
||||
- **Despliegue en Airflow**: `04_DESPLIEGUE_AIRFLOW.md` + `03_CONFIGURACION_VARIABLES.md`.
|
||||
- **Desarrollo / mantenimiento**: `05_FLUJO_Y_ARQUITECTURA.md` + `06_REFERENCIA_TECNICA_SAVEURL.md`.
|
||||
- **Seguridad**: `09_SEGURIDAD_Y_SECRETS.md`.
|
||||
|
||||
## Documentos
|
||||
|
||||
1. `01_VISION_GENERAL.md`
|
||||
2. `02_INSTALACION_LOCAL.md`
|
||||
3. `03_CONFIGURACION_VARIABLES.md`
|
||||
4. `04_DESPLIEGUE_AIRFLOW.md`
|
||||
5. `05_FLUJO_Y_ARQUITECTURA.md`
|
||||
6. `06_REFERENCIA_TECNICA_SAVEURL.md`
|
||||
7. `07_RUNBOOK_OPERACION.md`
|
||||
8. `08_TROUBLESHOOTING.md`
|
||||
9. `09_SEGURIDAD_Y_SECRETS.md`
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# Visión general
|
||||
|
||||
## ¿Qué hace este proyecto?
|
||||
|
||||
El proyecto **lee listados de archivos desde un servidor FTP**, **construye URLs públicas** para esos archivos y **guarda las URLs en SQL Server** dentro de una tabla llamada `URL_FTP`.
|
||||
|
||||
Durante la ejecución también:
|
||||
|
||||
- publica eventos de ejecución en **Kafka** (topic `executions_logs`)
|
||||
- envía una **notificación por webhook** ante errores (mensaje tipo “alerta pipeline”)
|
||||
|
||||
El entrypoint operativo está pensado para ejecutarse desde **Apache Airflow** a través de un DAG.
|
||||
|
||||
## Componentes principales
|
||||
|
||||
- `SaveUrl.py`
|
||||
- contiene la lógica de negocio (`run_save_url`)
|
||||
- implementa:
|
||||
- lectura FTP (`read_ftp`)
|
||||
- lectura SQL filtrada por carpeta (`read_table_where`)
|
||||
- inserción en SQL en lotes (`insert_records`)
|
||||
- logger a Kafka (`KafkaLogger`)
|
||||
- `dag_save_url_ftp.py`
|
||||
- define el DAG `save_url_ftp` y ejecuta `run_save_url`
|
||||
|
||||
## Entornos y configuración
|
||||
|
||||
Este proyecto se configura con **variables de Airflow** (Airflow Variables). Aunque el `README.md` menciona `.env`, el código actual usa `airflow.models.Variable.get(...)` para obtener:
|
||||
|
||||
- credenciales SQL Server
|
||||
- credenciales FTP
|
||||
- configuración Kafka (incluyendo SSL/SASL)
|
||||
- URL de webhook de notificación
|
||||
|
||||
## Frecuencia de ejecución
|
||||
|
||||
El DAG está programado con:
|
||||
|
||||
- cron: `0 */2 * * *` (**cada 2 horas**)
|
||||
|
||||
## Fuentes/“BDs” procesadas
|
||||
|
||||
En cada ejecución se itera sobre una lista fija (en `SaveUrl.py`) con entradas del tipo:
|
||||
|
||||
- nombre de carpeta base (ej. `glmpmi`)
|
||||
- `identity_id` usado para parsear el nombre de archivo/URL
|
||||
- nombre de “database” lógico usado para logs (ej. `glm_pmi`)
|
||||
- `database_id` (para construir `procedure_id` del log)
|
||||
|
||||
Para cada entrada se calcula un `code_folder` con el mes actual (formato `MM`), por ejemplo: `glmpmi04`.
|
||||
|
||||
## Salida principal
|
||||
|
||||
- **Tabla destino**: `URL_FTP` (SQL Server)
|
||||
- **Campos calculados por registro** (a partir de la URL):
|
||||
- `Folder`
|
||||
- `Date`
|
||||
- `IDcodigo`
|
||||
- `ObjectCode`
|
||||
- `TaskName`
|
||||
- `URL`
|
||||
|
||||
## Señales de observabilidad
|
||||
|
||||
- Logs en Airflow (logger estándar de Python)
|
||||
- Logs “estructurados” en Kafka en `executions_logs`
|
||||
- Notificación por webhook en fallos de FTP/SQL/INSERT
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# Instalación local (desarrollo / diagnóstico)
|
||||
|
||||
## Objetivo
|
||||
|
||||
Aunque el flujo está pensado para ejecutarse en Airflow, esta guía sirve para:
|
||||
|
||||
- reproducir fallos fuera del scheduler
|
||||
- validar conectividad a FTP/SQL/Kafka
|
||||
- ejecutar pruebas manuales de parsing de URLs
|
||||
|
||||
## Requisitos
|
||||
|
||||
- Windows (PowerShell) o Linux/macOS con Python 3.x
|
||||
- Driver ODBC para SQL Server (recomendado: **ODBC Driver 18 for SQL Server**)
|
||||
- Acceso de red a:
|
||||
- servidor FTP (`FTP_000oqta_SERVER`)
|
||||
- SQL Server (`DB_MSSQ_SERVER`)
|
||||
- Kafka (`BOOTSTRAP_SERVERS` o `BOOTSTRAP_SERVERS_DOCKER`, según corresponda)
|
||||
|
||||
## Crear entorno virtual
|
||||
|
||||
En PowerShell:
|
||||
|
||||
```powershell
|
||||
cd d:\ProgramGLM\SaveUrl
|
||||
python -m venv env
|
||||
.\env\Scripts\Activate.ps1
|
||||
python -m pip install --upgrade pip
|
||||
```
|
||||
|
||||
## Instalar dependencias
|
||||
|
||||
El código importa al menos:
|
||||
|
||||
- `requests`
|
||||
- `pyodbc`
|
||||
- `kafka` (librería `kafka-python` / `kafka-python-ng`)
|
||||
- `apache-airflow` (porque `SaveUrl.py` usa `airflow.models.Variable`)
|
||||
|
||||
Instalación típica (ajusta según tu entorno):
|
||||
|
||||
```powershell
|
||||
pip install requests pyodbc kafka-python apache-airflow
|
||||
```
|
||||
|
||||
> Nota: instalar Airflow localmente en Windows puede ser complejo. Si tu objetivo es **solo** ejecutar localmente, considera ejecutar en un contenedor Linux o adaptar temporalmente el código para leer variables desde `os.getenv`/`.env`. Esta documentación **no modifica** el código.
|
||||
|
||||
## Variables para ejecución local
|
||||
|
||||
El `SaveUrl.py` actual lee configuración desde **Airflow Variables** (`Variable.get`). Para ejecutar fuera de Airflow tienes 2 opciones:
|
||||
|
||||
1. **Ejecutar dentro de un entorno Airflow** (recomendado) y definir las Variables.
|
||||
2. **Crear un “shim” local**: arrancar un script wrapper que inicialice Airflow y cargue Variables. (Esto depende de cómo esté configurado tu Airflow local).
|
||||
|
||||
Por eso, para validación local rápida sin Airflow, lo más útil suele ser:
|
||||
|
||||
- probar funciones aisladas (FTP/SQL) en un REPL, o
|
||||
- ejecutar dentro de un contenedor Airflow/Composer donde ya existan las Variables.
|
||||
|
||||
## Ejecución manual (si ya estás dentro de Airflow)
|
||||
|
||||
Si Airflow ya está instalado/configurado y el archivo está en el path correcto, puedes invocar:
|
||||
|
||||
```powershell
|
||||
python -c "from SaveUrl import run_save_url; run_save_url()"
|
||||
```
|
||||
|
||||
## Verificación mínima
|
||||
|
||||
- **FTP**: confirma que existe el path base esperado: `FTP_BASE_PATH` + `<code_folder>`
|
||||
- **SQL**: valida conexión ODBC al servidor y permisos de INSERT/SELECT sobre `URL_FTP`
|
||||
- **Kafka**: valida que el broker esté accesible y el topic `executions_logs` exista (o que el broker permita auto-create)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# Configuración (Airflow Variables)
|
||||
|
||||
## Fuente de configuración real
|
||||
|
||||
El código en `SaveUrl.py` obtiene configuración principalmente desde **Airflow Variables** usando `airflow.models.Variable.get(...)`.
|
||||
|
||||
Esto implica:
|
||||
|
||||
- Debes definir estas Variables en tu instancia de Airflow (UI o CLI).
|
||||
- El archivo `.env.example` sirve como **referencia de nombres**, pero **no es la fuente** en ejecución si corres bajo Airflow.
|
||||
|
||||
## Variables requeridas
|
||||
|
||||
### SQL Server
|
||||
|
||||
- `DB_MSSQ_SERVER`: hostname o IP del SQL Server
|
||||
- `DB_MSSQ_USER`: usuario
|
||||
- `DB_MSSQ_PASSWORD`: password
|
||||
|
||||
Notas:
|
||||
|
||||
- La conexión ODBC usa `ODBC Driver 18 for SQL Server`.
|
||||
- La tabla destino es fija: `URL_FTP`.
|
||||
- El nombre de “database” (catálogo) se toma de la lista `INFORMATIONS` dentro del código (ej. `glm_pmi`, `IS001`, etc.).
|
||||
|
||||
### FTP
|
||||
|
||||
- `FTP_000oqta_SERVER`
|
||||
- `FTP_000oqta_USER`
|
||||
- `FTP_000oqta_PASSWORD`
|
||||
|
||||
Notas:
|
||||
|
||||
- El código construye el path como `FTP_BASE_PATH + code_folder`.
|
||||
- `code_folder` se calcula como `<name_folder><MM>` (mes actual), por ejemplo `glmpmi04`.
|
||||
|
||||
### Kafka
|
||||
|
||||
- `BOOTSTRAP_SERVERS_DOCKER`: brokers accesibles desde el entorno Airflow (por ejemplo `kafka-kraft:9092`)
|
||||
- `KAFKA_USE_SSL`: `"True"` o `"False"`
|
||||
|
||||
Si `KAFKA_USE_SSL == "True"`, además se leen (desde variables de entorno del proceso, vía `os.getenv`, no Airflow Variables):
|
||||
|
||||
- `SECURITY_PROTOCOL` (ej. `SASL_SSL`)
|
||||
- `SASL_MECHANISM` (ej. `SCRAM-SHA-256`)
|
||||
- `SASL_PLAIN_USERNAME`
|
||||
- `SASL_PLAIN_PASSWORD`
|
||||
- `SSL_CAFILE` (path al CA dentro del contenedor/host)
|
||||
- `SSL_CHECK_HOSTNAME` (`True`/`False`)
|
||||
|
||||
> Importante: en el código actual, `BOOTSTRAP_SERVERS_DOCKER` y `KAFKA_USE_SSL` se obtienen como Airflow Variables, pero los demás parámetros de seguridad se obtienen desde `os.getenv`. En despliegues en contenedores, asegúrate de definir esas variables en el **entorno del scheduler/worker**.
|
||||
|
||||
### Webhook de notificación
|
||||
|
||||
- `WEBHOOK_URL_NOTIFICATION`: endpoint que acepta `POST` con JSON `{"message": "..."}`
|
||||
|
||||
## Plantilla `.env.example`
|
||||
|
||||
El archivo `.env.example` contiene los nombres esperados (útil para referencia), pero:
|
||||
|
||||
- el proyecto **no debería** versionar un `.env` real con secretos
|
||||
- en Airflow, se recomienda gestionar secretos con backend (Vault/Secrets Manager/etc.) o Variables protegidas
|
||||
|
||||
## Validación recomendada (pre-flight)
|
||||
|
||||
Antes de activar el DAG en producción, valida:
|
||||
|
||||
- Variables existen (sin typos)
|
||||
- SQL user tiene `SELECT` e `INSERT` sobre `URL_FTP`
|
||||
- FTP credenciales permiten `NLST` sobre el path base
|
||||
- Kafka accesible desde el contenedor/worker que ejecuta el DAG
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Despliegue en Apache Airflow
|
||||
|
||||
## Qué se despliega
|
||||
|
||||
- DAG: `dag_save_url_ftp.py`
|
||||
- Módulo Python: `SaveUrl.py` (debe ser importable por el DAG)
|
||||
|
||||
## Ruta esperada por el DAG
|
||||
|
||||
En `dag_save_url_ftp.py` se define un `sys.path.insert`:
|
||||
|
||||
- `sys.path.insert(0, '/opt/airflow/dags/save_url_ftp')`
|
||||
|
||||
Por tanto, dentro del contenedor/host donde corre Airflow, se espera:
|
||||
|
||||
- `/opt/airflow/dags/save_url_ftp/SaveUrl.py`
|
||||
- `/opt/airflow/dags/save_url_ftp/dag_save_url_ftp.py`
|
||||
|
||||
> Si tu instalación usa otra ruta de `dags_folder`, ajusta el montaje/volumen para que esa estructura exista dentro del runtime.
|
||||
|
||||
## Parámetros del DAG
|
||||
|
||||
En `dag_save_url_ftp.py`:
|
||||
|
||||
- `dag_id`: `save_url_ftp`
|
||||
- schedule: `0 */2 * * *` (cada 2 horas)
|
||||
- `retries`: 2
|
||||
- `retry_delay`: 5 minutos
|
||||
- `catchup`: `False`
|
||||
- `max_active_runs`: 1
|
||||
|
||||
Estructura de tareas:
|
||||
|
||||
- `inicio` (EmptyOperator) → `save_url_ftp` (PythonOperator) → `fin` (EmptyOperator)
|
||||
|
||||
## Variables de Airflow necesarias
|
||||
|
||||
Define las Variables descritas en `03_CONFIGURACION_VARIABLES.md`.
|
||||
|
||||
En particular, `SaveUrl.py` requiere:
|
||||
|
||||
- `WEBHOOK_URL_NOTIFICATION`
|
||||
- `BOOTSTRAP_SERVERS_DOCKER`
|
||||
- `KAFKA_USE_SSL`
|
||||
- `FTP_000oqta_SERVER`, `FTP_000oqta_USER`, `FTP_000oqta_PASSWORD`
|
||||
- `DB_MSSQ_USER`, `DB_MSSQ_PASSWORD`, `DB_MSSQ_SERVER`
|
||||
|
||||
## Permisos y conectividad
|
||||
|
||||
- **Worker/Scheduler** que ejecute el PythonOperator debe tener:
|
||||
- salida a red hacia FTP, SQL Server y Kafka
|
||||
- driver ODBC instalado (si se usa `pyodbc` en el worker)
|
||||
|
||||
## Operación esperada en Airflow
|
||||
|
||||
Durante la ejecución, el task `save_url_ftp` escribe logs tipo:
|
||||
|
||||
- inicio/final
|
||||
- estado por “BD” lógica (cada item de `INFORMATIONS`)
|
||||
- conteos: archivos FTP, registros existentes, registros a insertar
|
||||
|
||||
Si ocurre un error en FTP/SQL/INSERT:
|
||||
|
||||
- registra `FAILED` en Kafka
|
||||
- envía webhook con el mensaje de alerta
|
||||
- continúa con la siguiente “BD” (no falla toda la corrida por un único item)
|
||||
|
||||
## Checklist de despliegue
|
||||
|
||||
- DAG visible y sin errores de importación
|
||||
- `SaveUrl.py` importable desde la ruta insertada en `sys.path`
|
||||
- Variables creadas (y protegidas si aplica)
|
||||
- Driver ODBC disponible en el runtime
|
||||
- Conectividad validada (FTP/SQL/Kafka/webhook)
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# Flujo y arquitectura
|
||||
|
||||
## Flujo end-to-end
|
||||
|
||||
El flujo, por cada entrada en `INFORMATIONS`, es:
|
||||
|
||||
1. Calcular `code_folder = <name_folder><MM>` (mes actual).
|
||||
2. Leer lista de archivos del FTP en `FTP_BASE_PATH + code_folder`.
|
||||
3. Convertir paths FTP a URLs públicas y **parsear metadatos** desde la URL.
|
||||
4. Leer desde SQL Server la tabla `URL_FTP` filtrando por `Folder LIKE %code_folder%`.
|
||||
5. Calcular diferencia por `IDcodigo`:
|
||||
- insertar solo los registros nuevos (los no existentes en SQL).
|
||||
6. Emitir logs a Kafka y escribir logs en Airflow.
|
||||
7. En caso de error, registrar `FAILED` y notificar por webhook; continuar con el siguiente item.
|
||||
|
||||
## Diagrama (alto nivel)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Airflow DAG save_url_ftp] --> B[PythonOperator: run_save_url]
|
||||
B --> C{Por cada item en INFORMATIONS}
|
||||
C --> D[FTP: nlst(path)]
|
||||
D --> E[Construir URL pública + parse]
|
||||
E --> F[SQL: SELECT por Folder]
|
||||
F --> G{Comparar IDcodigo}
|
||||
G -->|Nuevos| H[SQL: INSERT batches]
|
||||
G -->|Sin nuevos| I[Sin INSERT]
|
||||
D --> K[Kafka: executions_logs]
|
||||
F --> K
|
||||
H --> K
|
||||
B --> L[Webhook: alerta en errores]
|
||||
```
|
||||
|
||||
## Detalles del parsing (URL → campos)
|
||||
|
||||
`read_ftp()`:
|
||||
|
||||
- obtiene `file_names` via `ftp.nlst(path)`
|
||||
- construye URL pública como:
|
||||
- `FTP_BASE_URL + f.replace('/www', '')`
|
||||
- por cada URL calcula:
|
||||
- `Folder`: segmento `code_folder` dentro de la URL
|
||||
- `Date`: substring encontrado con patrón `_{YYYY}-{MM}`
|
||||
- `IDcodigo`: substring que empieza donde aparece `identity_id` y toma 24 caracteres
|
||||
- `ObjectCode`: substring entre `Date` y `IDcodigo`
|
||||
- `TaskName`: substring entre el fin de `code_folder + "/"` y el `_YYYY-MM...`
|
||||
- `URL`: la URL pública final
|
||||
|
||||
Esto asume que los nombres de archivos/paths en el FTP siguen un patrón consistente, por ejemplo:
|
||||
|
||||
- contiene `_<YYYY>-<MM>` como parte del nombre
|
||||
- contiene el `identity_id` en el nombre
|
||||
|
||||
## Interacción con SQL Server
|
||||
|
||||
### Lectura
|
||||
|
||||
`read_table_where()` ejecuta:
|
||||
|
||||
- `SELECT * FROM [URL_FTP] WHERE Folder LIKE '%<code_folder>%'`
|
||||
|
||||
Luego construye una lista de dicts con todas las columnas.
|
||||
|
||||
### Inserción
|
||||
|
||||
`insert_records()`:
|
||||
|
||||
- inserta en lotes de 1000 (`executemany`)
|
||||
- arma la lista de columnas desde las llaves del primer dict, excluyendo:
|
||||
- `ID`
|
||||
- `ExportDate`
|
||||
|
||||
Implicaciones:
|
||||
|
||||
- La tabla `URL_FTP` debe tener (como mínimo) columnas que coincidan con las llaves calculadas por `read_ftp()` (o defaults/allow null).
|
||||
- Si existen columnas extra en la tabla, el código **no** las insertará a menos que estén presentes en el dict.
|
||||
|
||||
## Observabilidad (Kafka)
|
||||
|
||||
El proyecto envía logs “estructurados” a Kafka con campos como:
|
||||
|
||||
- `database`, `procedure_id`, `procedure_name`
|
||||
- `procedure_type` (fijo: `PYTHON_APP`)
|
||||
- `status` (`STARTING`, `SUCCESS`, `FAILED`, `FINISHED`)
|
||||
- `message`, `executed_by`, `source`
|
||||
- `target_date`, `date`
|
||||
- `extra_info` (serializado a string)
|
||||
|
||||
El `procedure_id` se construye como:
|
||||
|
||||
- `<database_id>` + `PROCEDURE_ID_SUFFIX`
|
||||
|
||||
## Decisiones y limitaciones actuales
|
||||
|
||||
- **Configuración mezclada**: parte se lee como Airflow Variables y parte como variables de entorno (`os.getenv`) para Kafka SSL/SASL.
|
||||
- **Fechas**: usa la fecha/hora local del worker (`datetime.now()`), no el `execution_date` de Airflow.
|
||||
- **No hay deduplicación por URL**: la deduplicación se hace por `IDcodigo` únicamente.
|
||||
- **Errores por item**: si falla un item de `INFORMATIONS`, el script continúa con el siguiente.
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
# Referencia técnica (`SaveUrl.py`)
|
||||
|
||||
## Constantes relevantes
|
||||
|
||||
- `APP_NAME`: `PYTHON_CREATE_URL-FTP_APP`
|
||||
- `VERSION`: `2.0.0`
|
||||
- `PROCEDURE_ID_SUFFIX`: `000301900001`
|
||||
- `FTP_BASE_URL`: `https://000oqta.rcomhost.com`
|
||||
- `FTP_BASE_PATH`: `/www/imglm/2026/`
|
||||
- `WEBHOOK_URL`: se obtiene desde Airflow Variable `WEBHOOK_URL_NOTIFICATION`
|
||||
- `INFORMATIONS`: lista de fuentes a procesar (carpeta, identity_id, database lógico, database_id)
|
||||
|
||||
## Clase `KafkaLogger`
|
||||
|
||||
### Responsabilidad
|
||||
|
||||
Publica logs de ejecución a Kafka en el topic:
|
||||
|
||||
- `executions_logs`
|
||||
|
||||
### Inicialización
|
||||
|
||||
Parámetros:
|
||||
|
||||
- `source`: string (se usa `f"{APP_NAME}_{VERSION}"`)
|
||||
- `bootstrap_servers`: lista/string de brokers
|
||||
- `use_ssl`: boolean
|
||||
|
||||
Comportamiento:
|
||||
|
||||
- serializa `value` como JSON (`json.dumps(...).encode('utf-8')`)
|
||||
- si `use_ssl` es `True`, agrega configuración SASL/SSL desde `os.getenv`
|
||||
|
||||
### Método `log(...)`
|
||||
|
||||
Firma (conceptual):
|
||||
|
||||
- `log(database, db_id, procedure_name, status, message, executed_by, target_date, date, extra_info=None)`
|
||||
|
||||
Payload enviado a Kafka (campos clave):
|
||||
|
||||
- `database`: nombre lógico (ej. `glm_pmi`)
|
||||
- `procedure_id`: `str(db_id) + PROCEDURE_ID_SUFFIX`
|
||||
- `procedure_name`: texto como `Generacion de URL <db>`
|
||||
- `procedure_type`: `PYTHON_APP`
|
||||
- `status`: `STARTING` | `SUCCESS` | `FAILED` | `FINISHED`
|
||||
- `message`: descripción del evento
|
||||
- `executed_by`: usuario SQL (`DB_MSSQ_USER`)
|
||||
- `source`: `source` del logger
|
||||
- `extra_info`: string (sanitiza comillas simples)
|
||||
- `target_date`: fecha objetivo (`YYYY-MM-DD`)
|
||||
- `date`: timestamp (string)
|
||||
|
||||
## Función `read_ftp(...)`
|
||||
|
||||
### Responsabilidad
|
||||
|
||||
Conecta al FTP y lista archivos en:
|
||||
|
||||
- `FTP_BASE_PATH + code_folder`
|
||||
|
||||
Luego construye URLs públicas y parsea metadatos en un dict.
|
||||
|
||||
Parámetros:
|
||||
|
||||
- `ftp_server`, `ftp_user`, `ftp_password`
|
||||
- `code_folder`: carpeta calculada (`<name_folder><MM>`)
|
||||
- `identity_id`: string para ubicar `IDcodigo` dentro del nombre
|
||||
- `year`, `month`: se usan para encontrar `_{YYYY}-{MM}`
|
||||
|
||||
Salida:
|
||||
|
||||
- `list[dict]` con llaves: `Folder`, `Date`, `IDcodigo`, `ObjectCode`, `TaskName`, `URL`
|
||||
|
||||
Errores esperados:
|
||||
|
||||
- fallo de login o conectividad FTP
|
||||
- path inexistente (depende de cómo responda `nlst`)
|
||||
- parsing incorrecto si el nombre no contiene los patrones esperados
|
||||
|
||||
## Función `read_table_where(...)`
|
||||
|
||||
### Responsabilidad
|
||||
|
||||
Lee registros existentes en SQL Server desde una tabla, filtrando por `Folder`.
|
||||
|
||||
Query:
|
||||
|
||||
- `SELECT * FROM [URL_FTP] WHERE Folder LIKE '%<folder>%'`
|
||||
|
||||
Salida:
|
||||
|
||||
- lista de dicts (columna → valor)
|
||||
|
||||
Errores esperados:
|
||||
|
||||
- driver ODBC no instalado en runtime
|
||||
- credenciales inválidas / permisos insuficientes
|
||||
- tabla inexistente
|
||||
|
||||
## Función `insert_records(...)`
|
||||
|
||||
### Responsabilidad
|
||||
|
||||
Inserta registros en SQL Server en lotes de 1000.
|
||||
|
||||
Detalles:
|
||||
|
||||
- toma columnas desde el primer elemento del `data`
|
||||
- excluye `ID` y `ExportDate` si existen en el dict
|
||||
- arma un `INSERT INTO [URL_FTP] ([col1],...) VALUES (?,...)`
|
||||
|
||||
Salida:
|
||||
|
||||
- cantidad de registros insertados (int)
|
||||
|
||||
Errores esperados:
|
||||
|
||||
- tipos incompatibles con schema SQL
|
||||
- restricciones (NOT NULL, UNIQUE) no satisfechas
|
||||
- problemas de red/conexión
|
||||
|
||||
## Función `run_save_url(**context)`
|
||||
|
||||
### Responsabilidad
|
||||
|
||||
Orquesta toda la ejecución:
|
||||
|
||||
- inicializa `KafkaLogger`
|
||||
- obtiene Variables de Airflow (FTP/SQL/Kafka/webhook)
|
||||
- recorre `INFORMATIONS`
|
||||
- para cada item:
|
||||
- `read_ftp` → `read_table_where` → diff por `IDcodigo` → `insert_records`
|
||||
- logging a Kafka por pasos
|
||||
- notificación por webhook si hay errores
|
||||
|
||||
### Notificación (webhook)
|
||||
|
||||
En errores, construye un texto (formato “ALERTA PIPELINE”) y hace:
|
||||
|
||||
- `POST <WEBHOOK_URL_NOTIFICATION>` con JSON: `{"message": "<texto>"}`
|
||||
|
||||
### Consideraciones Airflow
|
||||
|
||||
- El parámetro `**context` permite que el callable sea usado por `PythonOperator`.
|
||||
- La función usa `datetime.now()` (no usa `execution_date` de Airflow).
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Runbook de operación
|
||||
|
||||
## Objetivo operacional
|
||||
|
||||
Mantener la sincronización:
|
||||
|
||||
- FTP (archivos del mes/carpeta) → SQL Server (`URL_FTP`)
|
||||
|
||||
con observabilidad en Kafka y alerta por webhook.
|
||||
|
||||
## Qué esperar en una ejecución “sana”
|
||||
|
||||
Por cada item en `INFORMATIONS` se esperan, en logs de Airflow:
|
||||
|
||||
- `✅ FTP ... <N> archivos en <code_folder>`
|
||||
- `✅ SQL ... <M> registros existentes`
|
||||
- `ℹ️ DIFF ... A insertar: <K>`
|
||||
- `✅ INSERT ... Insertados: <K>` (o warning de “Sin registros nuevos”)
|
||||
|
||||
Al final:
|
||||
|
||||
- `⏹ FINALIZADO ...`
|
||||
|
||||
## Inputs críticos (pre-flight)
|
||||
|
||||
- **Airflow Variables** existen y tienen valores válidos:
|
||||
- FTP: `FTP_000oqta_SERVER`, `FTP_000oqta_USER`, `FTP_000oqta_PASSWORD`
|
||||
- SQL: `DB_MSSQ_SERVER`, `DB_MSSQ_USER`, `DB_MSSQ_PASSWORD`
|
||||
- Kafka: `BOOTSTRAP_SERVERS_DOCKER`, `KAFKA_USE_SSL`
|
||||
- Webhook: `WEBHOOK_URL_NOTIFICATION`
|
||||
- El worker donde corre el DAG tiene:
|
||||
- driver ODBC 18 instalado
|
||||
- conectividad a FTP/SQL/Kafka/webhook
|
||||
|
||||
## Procedimiento ante incidentes (rápido)
|
||||
|
||||
1. Revisar logs del task `save_url_ftp` en Airflow.
|
||||
2. Identificar el “BD lógico” afectado (ej. `glm_pmi`, `IS001`, etc.).
|
||||
3. Clasificar el fallo:
|
||||
- FTP (login, path, listing)
|
||||
- SQL (conexión, permisos, tabla)
|
||||
- INSERT (schema/constraint)
|
||||
- Kafka (broker/SSL/SASL)
|
||||
- Webhook (endpoint caído / 4xx/5xx)
|
||||
4. Ejecutar acciones según tipo (ver `08_TROUBLESHOOTING.md`).
|
||||
|
||||
## Operación programada del DAG
|
||||
|
||||
El DAG corre cada 2 horas (`0 */2 * * *`).
|
||||
|
||||
Si necesitas ejecución manual:
|
||||
|
||||
- Ejecuta el task `save_url_ftp` desde la UI de Airflow (Trigger DAG / Run Task).
|
||||
|
||||
## Indicadores recomendados (SLOs simples)
|
||||
|
||||
- **Frescura**: últimas URLs del FTP aparecen en SQL < 2–4 horas.
|
||||
- **Cobertura**: por cada item, `A insertar` tiende a 0 tras una corrida exitosa.
|
||||
- **Errores**: tasa de `FAILED` por item debe ser baja; si sube, revisar conectividad o cambios de naming en FTP.
|
||||
|
||||
## Mantenimiento preventivo
|
||||
|
||||
- Revisar que `FTP_BASE_PATH` (año) sea correcto cuando cambie el año.
|
||||
- Verificar que las rutas/certificados SSL (si aplica) sean válidos dentro del contenedor.
|
||||
- Confirmar que el naming en FTP siga el patrón esperado (contiene `_{YYYY}-{MM}` e `identity_id`).
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
# Troubleshooting
|
||||
|
||||
## 1) El DAG no aparece o falla al importarse
|
||||
|
||||
**Síntomas**
|
||||
|
||||
- Airflow muestra “ImportError” o el DAG no se lista.
|
||||
|
||||
**Causas comunes**
|
||||
|
||||
- La ruta `'/opt/airflow/dags/save_url_ftp'` no existe dentro del contenedor.
|
||||
- `SaveUrl.py` no está en esa carpeta (o tiene otro nombre).
|
||||
- Dependencias no instaladas en el entorno del worker (ej. `pyodbc`, `kafka`).
|
||||
|
||||
**Acciones**
|
||||
|
||||
- Verifica que existan los archivos:
|
||||
- `/opt/airflow/dags/save_url_ftp/dag_save_url_ftp.py`
|
||||
- `/opt/airflow/dags/save_url_ftp/SaveUrl.py`
|
||||
- Revisa el scheduler logs por import errors.
|
||||
|
||||
## 2) Error conectando al FTP
|
||||
|
||||
**Síntomas**
|
||||
|
||||
- `❌ FTP | ...` en logs
|
||||
- notificación por webhook con “Error leyendo FTP folder ...”
|
||||
|
||||
**Causas comunes**
|
||||
|
||||
- Credenciales incorrectas (`FTP_000oqta_USER` / `FTP_000oqta_PASSWORD`)
|
||||
- Host incorrecto (`FTP_000oqta_SERVER`)
|
||||
- Firewall / DNS / ruteo
|
||||
- La carpeta del mes no existe (`<name_folder><MM>`)
|
||||
|
||||
**Acciones**
|
||||
|
||||
- Probar conectividad desde el mismo runtime (worker) hacia el host FTP.
|
||||
- Confirmar que `FTP_BASE_PATH` + `code_folder` existe en el servidor.
|
||||
- Verificar si el servidor cambió estructura para 2027/otro año (porque `FTP_BASE_PATH` incluye `2026`).
|
||||
|
||||
## 3) Error leyendo SQL Server
|
||||
|
||||
**Síntomas**
|
||||
|
||||
- `❌ SQL | ...` en logs
|
||||
- webhook “Error leyendo URLs ...”
|
||||
|
||||
**Causas comunes**
|
||||
|
||||
- Driver ODBC faltante en el worker (`ODBC Driver 18 for SQL Server`)
|
||||
- Credenciales inválidas o permisos insuficientes
|
||||
- SQL Server inaccesible desde la red del worker
|
||||
- Tabla `URL_FTP` inexistente o en otro schema/db esperado
|
||||
|
||||
**Acciones**
|
||||
|
||||
- Verificar instalación del driver ODBC en el contenedor/host.
|
||||
- Validar que el usuario tenga `SELECT` sobre `URL_FTP`.
|
||||
- Confirmar nombre de la base de datos: se toma de `INFORMATIONS` (ej. `glm_pmi`, `IS001`).
|
||||
|
||||
## 4) Error insertando (schema/constraints)
|
||||
|
||||
**Síntomas**
|
||||
|
||||
- `❌ INSERT | ...` en logs
|
||||
- webhook “Error insertando URLs ...”
|
||||
|
||||
**Causas comunes**
|
||||
|
||||
- La tabla tiene columnas NOT NULL sin default que el dict no provee.
|
||||
- Tipos incompatibles (ej. `Date` espera datetime pero recibe string).
|
||||
- Restricción UNIQUE (por `IDcodigo`, `URL`, u otra combinación) que el código no contempla.
|
||||
|
||||
**Acciones**
|
||||
|
||||
- Revisar definición de `URL_FTP` y confirmar que acepta las columnas:
|
||||
- `Folder`, `Date`, `IDcodigo`, `ObjectCode`, `TaskName`, `URL`
|
||||
- Confirmar tipos: el código inserta strings.
|
||||
- Si existe una columna “ExportDate”, el código NO la inserta (la excluye si aparece en dict).
|
||||
|
||||
## 5) No inserta nada, pero debería
|
||||
|
||||
**Síntomas**
|
||||
|
||||
- `A insertar: 0` repetidamente, aunque hay archivos nuevos en FTP.
|
||||
|
||||
**Causas comunes**
|
||||
|
||||
- `IDcodigo` se parsea mal (mismo valor para distintos archivos) por cambios de naming.
|
||||
- La consulta SQL `Folder LIKE %code_folder%` trae más registros de los necesarios y “tapa” IDs.
|
||||
- El FTP lista rutas que no cumplen el patrón y el parse genera IDs inesperados.
|
||||
|
||||
**Acciones**
|
||||
|
||||
- Tomar una URL real del FTP y revisar manualmente el parsing:
|
||||
- ¿contiene `_{YYYY}-{MM}`?
|
||||
- ¿contiene el `identity_id` correcto?
|
||||
- ¿`IDcodigo` de 24 chars es correcto?
|
||||
- Comparar un `IDcodigo` “nuevo” con los existentes en SQL.
|
||||
|
||||
## 6) Kafka no recibe logs
|
||||
|
||||
**Síntomas**
|
||||
|
||||
- Logs de Airflow muestran fallos de Kafka o no hay eventos en `executions_logs`.
|
||||
|
||||
**Causas comunes**
|
||||
|
||||
- `BOOTSTRAP_SERVERS_DOCKER` incorrecto para el runtime del worker.
|
||||
- `KAFKA_USE_SSL=True` pero faltan variables de entorno `SECURITY_PROTOCOL`, `SASL_*`, `SSL_CAFILE`, etc.
|
||||
- Certificados/rutas inválidas dentro del contenedor.
|
||||
|
||||
**Acciones**
|
||||
|
||||
- Validar conectividad al broker desde el contenedor/worker.
|
||||
- Si SSL/SASL: asegurar que las variables están definidas como **env vars** del proceso.
|
||||
|
||||
## 7) Webhook no notifica
|
||||
|
||||
**Síntomas**
|
||||
|
||||
- Se ve el error en logs pero no llega alerta.
|
||||
|
||||
**Causas comunes**
|
||||
|
||||
- `WEBHOOK_URL_NOTIFICATION` no definido o es inválido.
|
||||
- Endpoint requiere auth/cabeceras adicionales (el código no las envía).
|
||||
- Restricciones de red del worker.
|
||||
|
||||
**Acciones**
|
||||
|
||||
- Probar un `POST` manual al webhook desde el mismo entorno.
|
||||
- Revisar respuesta HTTP (4xx/5xx) en logs si se captura (actualmente no se valida el status code).
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Seguridad y manejo de secretos
|
||||
|
||||
## No versionar secretos
|
||||
|
||||
El repositorio incluye `.gitignore` para ignorar:
|
||||
|
||||
- `.env`
|
||||
- `env/` (virtualenv)
|
||||
|
||||
Recomendación:
|
||||
|
||||
- Mantener **cualquier credencial** fuera del control de versiones.
|
||||
|
||||
## Dónde viven los secretos en este proyecto
|
||||
|
||||
El código en `SaveUrl.py` toma secretos principalmente desde:
|
||||
|
||||
- **Airflow Variables**:
|
||||
- SQL: `DB_MSSQ_USER`, `DB_MSSQ_PASSWORD`, `DB_MSSQ_SERVER`
|
||||
- FTP: `FTP_000oqta_USER`, `FTP_000oqta_PASSWORD`, `FTP_000oqta_SERVER`
|
||||
- Webhook: `WEBHOOK_URL_NOTIFICATION`
|
||||
- Kafka básico: `BOOTSTRAP_SERVERS_DOCKER`, `KAFKA_USE_SSL`
|
||||
- **Variables de entorno del proceso** (solo cuando `KAFKA_USE_SSL=True`):
|
||||
- `SECURITY_PROTOCOL`, `SASL_MECHANISM`
|
||||
- `SASL_PLAIN_USERNAME`, `SASL_PLAIN_PASSWORD`
|
||||
- `SSL_CAFILE`, `SSL_CHECK_HOSTNAME`
|
||||
|
||||
Implicación:
|
||||
|
||||
- Para Kafka con SSL/SASL, no basta con Airflow Variables: hay que configurar también **env vars** en el worker/scheduler (o usar un backend de secrets que las exponga como env vars).
|
||||
|
||||
## Recomendaciones de seguridad
|
||||
|
||||
- **Airflow Secrets Backend**: usar un backend (Vault/AWS Secrets Manager/Azure Key Vault/GCP Secret Manager) en vez de Variables planas.
|
||||
- **Rotación**:
|
||||
- FTP y SQL: rotación periódica y seguimiento de expiración.
|
||||
- Kafka: rotación de credenciales SASL y CA si cambia.
|
||||
- **Principio de mínimo privilegio**:
|
||||
- SQL user: permisos mínimos necesarios sobre `URL_FTP` (SELECT/INSERT).
|
||||
- FTP user: permisos mínimos para listar/leer rutas necesarias (NLST).
|
||||
- **TLS**:
|
||||
- si se habilita SSL en Kafka, asegurar que `SSL_CAFILE` apunte a un CA dentro del contenedor.
|
||||
|
||||
## Señales de filtración / exposición
|
||||
|
||||
Revisar que no exista:
|
||||
|
||||
- `.env` real en el repo
|
||||
- credenciales hardcodeadas en archivos `.py`
|
||||
- rutas de certificados locales de un desarrollador (ej. paths absolutos de Windows) en configuración de producción
|
||||
|
||||
## Checklist antes de producción
|
||||
|
||||
- Variables/secretos definidos en sistema seguro
|
||||
- `.env` excluido y no presente en artefactos de build
|
||||
- permisos SQL y FTP mínimos
|
||||
- endpoints (Kafka/webhook) accesibles solo desde redes necesarias
|
||||
|
||||
Reference in New Issue
Block a user