ETL Troubleshooting
Overview
Section titled “Overview”Guía de diagnóstico y resolución de problemas comunes en el pipeline ETL Python que sincroniza datos desde el SII y otras fuentes externas.
Referencia: ADR-003: ETL Architecture
Arquitectura ETL
Section titled “Arquitectura ETL”┌─────────────┐ ┌─────────────┐ ┌─────────────┐│ Sources │────▶│ ETL Python │────▶│ PostgreSQL ││ (SII, CSV) │ │ Pipeline │ │ (nostromo) │└─────────────┘ └─────────────┘ └─────────────┘ │ ▼ ┌─────────────┐ │ Logs │ │ /var/log/etl│ └─────────────┘Problemas Comunes
Section titled “Problemas Comunes”🔴 ETL No Inicia
Section titled “🔴 ETL No Inicia”Síntomas
Section titled “Síntomas”- Cron job no ejecuta
- Script falla al iniciar
- No hay logs recientes
Diagnóstico
Section titled “Diagnóstico”# Verificar cron configuradocrontab -l | grep etl
# Ejecutar manualmentecd /opt/nostromo/etlpython3 main.py --verbose
# Verificar permisosls -la /opt/nostromo/etl/Soluciones
Section titled “Soluciones”| Causa | Solución |
|---|---|
| Cron no configurado | crontab -e y agregar job |
| Python no encontrado | Usar path absoluto /usr/bin/python3 |
| Permisos insuficientes | chmod +x main.py |
| Virtual env no activado | source venv/bin/activate en cron |
Ejemplo cron correcto:
0 3 * * * cd /opt/nostromo/etl && source venv/bin/activate && python main.py >> /var/log/etl/cron.log 2>&1🔴 Error de Conexión a Base de Datos
Section titled “🔴 Error de Conexión a Base de Datos”Síntomas
Section titled “Síntomas”psycopg2.OperationalError: could not connect to serverConnection refusedAuthentication failed
Diagnóstico
Section titled “Diagnóstico”# Desde el servidor ETLpg_isready -h localhost -p 5432
# Verificar credencialescat /opt/nostromo/etl/.env | grep DB_
# Test de conexiónpsql -h localhost -U etl_user -d nostromo -c "SELECT 1;"Soluciones
Section titled “Soluciones”| Causa | Solución |
|---|---|
| PostgreSQL caído | sudo systemctl start postgresql |
| Credenciales incorrectas | Verificar .env vs pg_hba.conf |
| Firewall bloquea | sudo ufw allow from <ETL_IP> to any port 5432 |
| SSL requerido | Agregar sslmode=require a connection string |
🟡 ETL Lento
Section titled “🟡 ETL Lento”Síntomas
Section titled “Síntomas”- Job tarda más de 2 horas
- Alto uso de CPU/memoria
- Timeouts en queries
Diagnóstico
Section titled “Diagnóstico”# Ver queries activassudo -u postgres psql -c "SELECT pid, now() - query_start as duration, queryFROM pg_stat_activityWHERE state = 'active' AND query NOT LIKE '%pg_stat%'ORDER BY duration DESC;"
# Monitorear proceso ETLtop -p $(pgrep -f etl)Soluciones
Section titled “Soluciones”| Causa | Solución |
|---|---|
| Sin índices | Crear índices en tablas destino |
| Batch size pequeño | Aumentar ETL_BATCH_SIZE |
| Sin paralelismo | Habilitar ETL_PARALLEL_WORKERS |
| Lock contention | Ejecutar en horario de bajo tráfico |
Optimizaciones recomendadas:
ETL_BATCH_SIZE = 10000 # Default: 1000ETL_PARALLEL_WORKERS = 4 # Default: 1ETL_COMMIT_INTERVAL = 5000 # Commit cada N registros🟡 Datos Duplicados
Section titled “🟡 Datos Duplicados”Síntomas
Section titled “Síntomas”- Registros duplicados en tablas destino
- Conteos inconsistentes
- Errores de constraint unique
Diagnóstico
Section titled “Diagnóstico”-- Buscar duplicados por RUTSELECT rut, COUNT(*)FROM employeesGROUP BY rutHAVING COUNT(*) > 1;
-- Ver últimos insertsSELECT id, rut, created_atFROM employeesORDER BY created_at DESCLIMIT 20;Soluciones
Section titled “Soluciones”| Causa | Solución |
|---|---|
| Falta upsert | Usar ON CONFLICT DO UPDATE |
| ID no único | Agregar constraint UNIQUE |
| Re-ejecución accidental | Tracking de estado de jobs |
Patrón upsert correcto:
INSERT_QUERY = """INSERT INTO employees (rut, name, email)VALUES (%s, %s, %s)ON CONFLICT (rut) DO UPDATE SET name = EXCLUDED.name, email = EXCLUDED.email, updated_at = NOW()"""🟡 Datos Faltantes
Section titled “🟡 Datos Faltantes”Síntomas
Section titled “Síntomas”- Registros no importados
- Campos nulos inesperados
- Conteo menor al esperado
Diagnóstico
Section titled “Diagnóstico”# Verificar archivo fuentewc -l /opt/data/import_*.csv
# Comparar con registros importadospsql -c "SELECT COUNT(*) FROM staging.import_log WHERE status = 'error';"
# Ver errores específicostail -100 /var/log/etl/errors.logSoluciones
Section titled “Soluciones”| Causa | Solución |
|---|---|
| Encoding incorrecto | Forzar encoding='utf-8' o latin-1 |
| Formato fecha inválido | Normalizar fechas antes de insert |
| Validación fallida | Revisar reglas en validators.py |
| Archivo truncado | Verificar integridad de fuente |
Logging de errores:
def transform_row(row): try: return validate_and_transform(row) except ValidationError as e: logger.error(f"Row {row['id']} failed: {e}") error_log.append(row) return None # Skip row🟡 Error de API Externa (SII)
Section titled “🟡 Error de API Externa (SII)”Síntomas
Section titled “Síntomas”ConnectionError: Max retries exceededHTTPError: 503 Service UnavailableSSLError: certificate verify failed
Diagnóstico
Section titled “Diagnóstico”# Test de conectividadcurl -v https://api.sii.cl/health
# Verificar certificadosopenssl s_client -connect api.sii.cl:443 -servername api.sii.cl
# Ver rate limitsgrep "rate limit" /var/log/etl/sii.logSoluciones
Section titled “Soluciones”| Causa | Solución |
|---|---|
| SII caído | Reintentar con backoff exponencial |
| Rate limit | Reducir SII_REQUESTS_PER_MINUTE |
| Certificado expirado | Actualizar ca-certificates |
| IP bloqueada | Contactar SII soporte |
Implementar retry:
from tenacity import retry, stop_after_attempt, wait_exponential
@retry( stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=4, max=60))def fetch_from_sii(endpoint): response = requests.get(f"{SII_BASE_URL}/{endpoint}") response.raise_for_status() return response.json()🟡 Schema Mismatch
Section titled “🟡 Schema Mismatch”Síntomas
Section titled “Síntomas”Column "X" does not existValue too long for type character varying(50)Invalid input syntax for type integer
Diagnóstico
Section titled “Diagnóstico”-- Ver schema actual\d employees
-- Comparar con fuenteSELECT column_name, data_type, character_maximum_lengthFROM information_schema.columnsWHERE table_name = 'employees';Soluciones
Section titled “Soluciones”| Causa | Solución |
|---|---|
| Columna nueva en fuente | Agregar migración ALTER TABLE |
| Tipo incorrecto | Cast explícito en transformación |
| Longitud insuficiente | ALTER TABLE ... ALTER COLUMN ... TYPE VARCHAR(100) |
Logs y Monitoreo
Section titled “Logs y Monitoreo”Ubicación de Logs
Section titled “Ubicación de Logs”| Log | Path | Contenido |
|---|---|---|
| Main | /var/log/etl/main.log | Ejecución general |
| Errors | /var/log/etl/errors.log | Solo errores |
| SII | /var/log/etl/sii.log | Llamadas a API SII |
| Cron | /var/log/etl/cron.log | Output de cron jobs |
Comandos Útiles
Section titled “Comandos Útiles”# Ver últimos errorestail -f /var/log/etl/errors.log
# Buscar errores específicosgrep -i "connection" /var/log/etl/main.log | tail -20
# Contar errores por tipogrep -oP "ERROR: \K[^:]*" /var/log/etl/errors.log | sort | uniq -cAlertas
Section titled “Alertas”# Agregar a cron para alertar si ETL fallaRecuperación
Section titled “Recuperación”Rollback de Datos
Section titled “Rollback de Datos”-- Si ETL corrompió datos, rollback desde stagingBEGIN;
-- Borrar datos del díaDELETE FROM employeesWHERE updated_at >= CURRENT_DATE;
-- Restaurar desde staging/backupINSERT INTO employeesSELECT * FROM staging.employees_backupWHERE backup_date = CURRENT_DATE - 1;
COMMIT;Re-ejecutar ETL Parcial
Section titled “Re-ejecutar ETL Parcial”# Re-procesar solo un día específicopython main.py --date 2026-01-15
# Re-procesar solo un módulopython main.py --module indicadores
# Dry run (sin escribir a DB)python main.py --dry-runChecklist Pre-Run
Section titled “Checklist Pre-Run”- PostgreSQL accesible:
pg_isready - Credenciales válidas: verificar
.env - Espacio en disco:
df -h - APIs externas accesibles:
curl https://api.sii.cl - Backup del día anterior existe
- Logs rotados:
logrotate -f
Related Documentation
Section titled “Related Documentation”Changelog
Section titled “Changelog”| Fecha | Version | Cambios |
|---|---|---|
| 2026-01-18 | 1.0 | Guía inicial creada |