Hoja de Ruta
Contexto sobre el bundle ZPA
Cuando descargas un Support Bundle desde Zscaler ZPA (desde la app del cliente o desde la consola de administracion), obtienes un ZIP que contiene decenas de archivos de log. Estos logs cubren multiples componentes del cliente y pueden remontarse varios dias atras. El volumen puede ser abrumador.
El objetivo no es leer todo. Es encontrar rapidamente donde esta el problema.
FASE 1: Conocer la estructura del bundle
Estructura tipica de un Support Bundle ZPA
Al descomprimir el ZIP, te encuentras con una carpeta que contiene algo asi:
Zscaler-2026-05-11-13-13-21/
├── logs/
│ ├── zsa-tunnel.log ← Motor de tuneles (CRITICO)
│ ├── zsa-service.log ← Servicio Windows (CRITICO)
│ ├── zsa-tray.log ← Interfaz de usuario (SECUNDARIO)
│ ├── zsa-dns.log ← Cache DNS de ZPA (UTIL)
│ ├── zsa-proxy.log ← Proxy local dgwip.exe (UTIL)
│ ├── zsa-wfp.log ← Driver WFP kernel (AVANZADO)
│ ├── zsa-diag.log ← Diagnostico integrado (MUY UTIL)
│ ├── zsa-update.log ← Actualizaciones del cliente (BAJO PRIORIDAD)
│ ├── zsa-install.log ← Instalacion (SOLO si hubo problemas de instalacion)
│ └── zsa-misc.log ← Miscelanea (DESCARTABLE normalmente)
├── config/
│ ├── zpa_config.json ← Configuracion del cliente (UTIL)
│ └── policy_config.json ← Politicas cacheadas (UTIL)
├── network/
│ ├── netsh_wfp_show_filters.xml ← Filtros WFP (AVANZADO)
│ ├── netsh_advfirewall.log ← Firewall Windows (CONTEXTUAL)
│ └── ipconfig.txt ← Estado de red (UTIL)
└── system/
├── systeminfo.txt ← Info del sistema (UTIL)
├── tasklist.txt ← Procesos activos (UTIL)
└── drivers.txt ← Drivers cargados (UTIL)
Nota: La estructura exacta varia segun la version del cliente y la plataforma. En Linux/Mac la estructura es similar pero con paths distintos.
FASE 2: Priorizar - Que mirar primero
Nivel 0 - Visibilidad rapida (5 minutos)
Antes de leer nada en detalle, haz una pasada visual para entender la escala:
# En PowerShell, dentro de la carpeta del bundle
# Ver que archivos hay y cuanto miden
Get-ChildItem -Recurse -File | Sort-Object Length -Descending |
Select-Object @{N='SizeKB';E={[math]::Round($_.Length/1KB,1)}},
@{N='SizeMB';E={[math]::Round($_.Length/1MB,2)}},
FullName | Format-Table -AutoSize# En Linux/Mac
find . -type f -exec ls -lhS {} + | head -30Lo que buscas: Los archivos mas grandes son los que mas actividad tienen. Si zsa-tunnel.log tiene 50MB y zsa-tray.log tiene 200KB, ya sabes donde esta la accion.
Nivel 1 - Los 3 archivos que siempre miras primero (15 minutos)
En orden de prioridad:
1. zsa-tunnel.log - EL ARCHIVO MAS IMPORTANTE
Por que: Aqui se registran las conexiones de los micro-tuneles: establecimiento, errores, fallos DTLS, timeouts, desconexiones. Si algo falla en ZPA, se refleja aqui.
Que buscar de forma inmediata:
Errores criticos:
- "error" (case insensitive)
- "fail"
- "timeout"
- "disconnect"
- "BRK_MT_" (codigos de eventos de micro-tuneles)
Eventos de micro-tuneles:
- "BRK_MT_SETUP" (establecimiento de micro-tunel)
- "BRK_MT_CLOSED" (cierre de micro-tunel)
- "BRK_MT_SETUP_FAIL" (fallo de micro-tunel)
- "tag" (identificador de cada micro-tunel)
Protocolo:
- "DTLS" (busca si esta funcionando o fallando)
- "TLS" (fallback - indica que DTLS no funciona)
- "fallback" (cambio de DTLS a TLS)
Conexion con Brokers:
- "broker" (estados de conexion)
- "87.58" / "165.225" (IPs de Brokers observadas)
- "connected" / "disconnected"
2. zsa-service.log - EL SERVICIO WINDOWS
Por que: Gestiona el ciclo de vida del cliente, sesiones de usuario, y eventos del sistema. Si el servicio se cuelga, se reinicia, o pierde la sesion, aqui lo veras.
Que buscar:
Eventos de sesion:
- "session" (creacion, destruccion)
- "login" / "logout"
- "user"
Estados del servicio:
- "start" / "stop" / "restart"
- "crash" / "exception"
- "timeout"
Politicas:
- "policy" (carga, actualizacion, errores)
- "FQDN" (validacion de hostname)
- "FQDN_NO_MATCH" (hostname no valido - CRITICO)
- "register" / "deregister"
3. zsa-diag.log - EL DIAGNOSTICO
Por que: Este archivo contiene los resultados del diagnostico interno de ZPA. Incluye tests de conectividad, estado de tuneles, y problemas detectados automaticamente.
Que buscar:
- "PASS" / "FAIL" (resultados de tests)
- "connectivity" (tests de conexion)
- "dtls" / "tls" (estado del protocolo)
- "dns" (resolucion DNS)
- "wfp" (estado del driver)
- "sublayer" (conflictos con otros drivers)
Nivel 2 - Archivos de contexto (10 minutos)
Despues de entender los errores, necesitas contexto:
| Archivo | Que te dice | Que buscar |
|---|---|---|
zsa-dns.log | Como ZPA resuelve dominios internos | Dominios que fallan, TTLs, IPs virtuales 100.64.x.x |
zsa-proxy.log | Trafico a traves del proxy local 127.0.0.1:3128 | Conexiones rechazadas, errores de proxy |
ipconfig.txt | Estado de red del equipo | Adaptadores VPN conflictivos, DNS servers, IPs |
zpa_config.json | Configuracion activa del cliente | enableSplitVpnTN, enrollmentCert, versiones |
tasklist.txt | Procesos activos en el momento | Otros agentes VPN (Ivanti, Cisco, etc.), antivirus |
drivers.txt | Drivers cargados en kernel | zsawdrv.sys (ZPA), drivers VPN de terceros |
Nivel 3 - Archivos avanzados (solo si es necesario)
| Archivo | Cuando lo miras | Complejidad |
|---|---|---|
netsh_wfp_show_filters.xml | Cuando sospechas conflicto WFP con otro agente | Alta - XML con miles de filtros |
zsa-wfp.log | Cuando los errores indican fallos del driver WFP | Alta - requiere conocimiento de kernel |
zsa-tray.log | Cuando el problema es visual (el icono no muestra estado correcto) | Baja |
zsa-update.log | Solo si sospechas que una actualizacion causo el problema | Baja |
zsa-install.log | Solo si el problema es la instalacion misma | Baja |
Nivel 4 - Archivos que normalmente descartas
zsa-misc.log → Informacion residual, rara vez util
zsa-install.log → Solo relevante si el problema es la instalacion
*.bak files → Copias de seguridad de versiones anteriores
*.tmp files → Archivos temporales de escritura
FASE 3: Filtrado manual con herramientas
3.1 Notepad++ (Windows)
Notepad++ es probablemente la herramienta mas accesible para analisis manual de logs.
Busqueda basica: Ctrl+F → Find in Files
Atajo: Ctrl + Shift + F
Find what: error
Directory: C:\ruta\al\bundle\logs
Filters: *.log
Search Mode: Normal
☑ Match case: NO (primera pasada)
Esto te da todas las lineas que contienen “error” en todos los archivos de log.
Busqueda avanzada: Find in Files con regex
Cambiar Search Mode a “Regular expression”:
# Buscar cualquier tipo de error o fallo
Find what: (?i)(error|fail|timeout|exception|crash)
# Buscar codigos de evento de micro-tuneles
Find what: BRK_MT_\w+
# Buscar IPs de red (formato x.x.x.x)
Find what: \b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b
# Buscar timestamps para una hora especifica (ej: entre 11:00 y 11:15)
Find what: 11:0[0-9]:\d{2}.*(?:error|fail|timeout)
# Buscar errores de DTLS
Find what: (?i)dtls.*(?:fail|error|timeout|fallback)
# Buscar eventos de FALLBACK a TLS (indica que DTLS no funciona)
Find what: (?i)tls.*fallback
# Buscar codigos de error numericos ZPA
Find what: (?:error|code)\s*[=:]\s*\d{4,5}
# Buscar todo lo relacionado con un tag/micro-tunel especifico
Find what: tag\s*65539
Paso a paso en Notepad++
Paso 1: Vista panoramica del archivo
Abre el archivo zsa-tunnel.log y usa View → Document Map (atajo: Alt + V, luego Document Map). Esto te muestra una miniatura del archivo donde puedes ver visualmente las zonas con mucho texto (errores suelen ser lineas largas).
Paso 2: Marcar todas las ocurrencias
Ctrl + F → pestaña “Mark” → escribe la palabra clave → “Mark All”. Esto colorea todas las lineas que coinciden. Luego puedes navegar entre ellas con los botones “Next” y “Previous”.
Paso 3: Extraer solo las lineas que interesan
Search → Find in Files → marca la opcion “Bookmark Line”. Despues, Search → Bookmark → Copy Bookmarked Lines. Esto copia solo las lineas relevantes a un nuevo documento para analisis separado.
Paso 4: Comparar dos versiones del mismo log
Si tienes un bundle de cuando funcionaba y otro de cuando falla:
Plugins → Compare → Compare (necesitas el plugin Compare, instalable via Plugin Manager).
Paso 5: Colorear palabras clave automaticamente
Settings → Style Configurator no permite keywords personalizadas, pero puedes usar el plugin “EnhanceAnyLexer” o simplemente usar el modo de lenguaje y configurar User Defined Language con colores para palabras clave como error, fail, timeout, BRK_MT_.
3.2 Visual Studio Code (alternativa superior a Notepad++)
VS Code es mejor que Notepad++ para archivos de log grandes porque:
- Abre archivos de cualquier tamano sin colgarse
- Busqueda regex rapida en multiples archivos
- Permite colapsar secciones por timestamps
- Tiene extensiones especificas para logs
Extension recomendada
Instala “Log File Highlighter” de emilast. Automaticamente colorea:
- Lineas con
ERROR/erroren rojo - Lineas con
WARN/warningen amarillo - Lineas con
INFOen verde - Timestamps en azul
Busqueda en VS Code
Ctrl + Shift + F (busqueda en todos los archivos)
# En el campo de busqueda:
error|fail|timeout|BRK_MT_.*FAIL
# Activa el icono de regex (.*)
# Activa "files to include" y escribe: *.log
# Para buscar en un rango de tiempo:
# Primero busca el timestamp, luego usa el panel de resultados
Multi-cursor para analisis rapido
Si necesitas extraer todos los valores de un campo (por ejemplo, todos los tag IDs):
Ctrl + F → busca "tag \d+"
Alt + Enter (selecciona todas las coincidencias)
Ctrl + C (copia todas)
3.3 PowerShell: Busqueda programatica
Para cuando necesitas filtrado mas sofisticado del que Notepad++ permite:
# === BUSQUEDA BASICA ===
# Buscar "error" en todos los .log del bundle
Get-ChildItem -Recurse -Filter "*.log" |
Select-String -Pattern "error" -CaseSensitive:$false |
Select-Object Filename, LineNumber, Line
# === BUSQUEDA POR RANGO TEMPORAL ===
# Solo errores entre 11:00 y 11:15
Get-Content .\logs\zsa-tunnel.log |
Where-Object { $_ -match "11:0[0-9]:" -and $_ -match "(?i)(error|fail|timeout)" }
# === EXTRAER TODOS LOS EVENTOS BRK_MT_ ===
Get-Content .\logs\zsa-tunnel.log |
Where-Object { $_ -match "BRK_MT_" } |
ForEach-Object {
if ($_ -match "(\d{2}:\d{2}:\d{2}).*?(BRK_MT_\w+).*?(?:tag\s*(\d+))?") {
[PSCustomObject]@{
Hora = $matches[1]
Evento = $matches[2]
Tag = if($matches[3]){$matches[3]}else{"N/A"}
}
}
} | Sort-Object Hora | Format-Table -AutoSize
# === CONTAR ERRORES POR TIPO ===
Get-Content .\logs\zsa-tunnel.log |
Where-Object { $_ -match "BRK_MT_" } |
ForEach-Object {
if ($_ -match "(BRK_MT_\w+)") { $matches[1] }
} | Group-Object | Sort-Object Count -Descending |
Select-Object Count, Name | Format-Table -AutoSize
# === BUSCAR TODOS LOS CODIGOS DE ERROR ===
Get-Content .\logs\zsa-tunnel.log |
Where-Object { $_ -match "(?i)error.*?(\d{4,5})" } |
ForEach-Object {
if ($_ -match "(\d{4,5})") { $matches[1] }
} | Group-Object | Sort-Object Count -Descending |
Select-Object Count, Name | Format-Table -AutoSize
# === BUSCAR CONFLICTOS WFP ===
Select-String -Path ".\logs\zsa-wfp.log" -Pattern "(?i)(sublayer|weight|conflict|interfere)" |
Select-Object LineNumber, Line
# === EXTRAER TODAS LAS IPs OBSERVADAS ===
Select-String -Path ".\logs\*.log" -Pattern "\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b" -AllMatches |
ForEach-Object { $_.Matches.Value } |
Sort-Object -Unique
# === BUSCAR FQDN_NO_MATCH ===
Select-String -Path ".\logs\*.log" -Pattern "FQDN" |
Select-Object Filename, LineNumber, Line3.4 Linux/Mac: grep, awk y sed
# === BUSQUEDA BASICA ===
# Buscar errores en todos los logs
grep -rni "error\|fail\|timeout" logs/*.log | head -50
# === BUSQUEDA POR TIPO DE EVENTO ===
# Todos los eventos de micro-tuneles
grep -n "BRK_MT_" logs/zsa-tunnel.log
# Solo fallos de micro-tuneles
grep -n "BRK_MT_.*FAIL" logs/zsa-tunnel.log
# Solo cierres de micro-tuneles
grep -n "BRK_MT_CLOSED" logs/zsa-tunnel.log
# === BUSQUEDA POR RANGO TEMPORAL ===
# Errores entre 11:00 y 11:15
grep -n "11:0[0-9]:" logs/zsa-tunnel.log | grep -i "error\|fail\|timeout"
# === CONTEO DE ERRORES ===
# Cuantos errores de cada tipo hay
grep "BRK_MT_" logs/zsa-tunnel.log | awk -F'BRK_MT_' '{print $2}' | awk '{print $1}' | sort | uniq -c | sort -rn
# === EXTRAER IPs ===
grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b" logs/*.log | sort -u
# === BUSQUEDA EN MULTIPLES ARCHIVOS ===
grep -rni "DTLS" logs/ --include="*.log"
# Buscar fallback DTLS -> TLS
grep -ni "fallback\|dtls.*tls" logs/zsa-tunnel.log
# === EXTRAER LINEAS POR TAG ESPECIFICO ===
grep "tag.*65539" logs/zsa-tunnel.log
# === AWK: Extraer errores con timestamp ===
awk '/11:0[0-9]:/ && /(error|fail|timeout)/' logs/zsa-tunnel.log
# === BUSQUEDA AVANZADA: Timeline de eventos ===
# Extraer todos los eventos en orden cronologico
grep -n "BRK_MT_\|error\|fail\|connected\|disconnected\|DTLS\|TLS\|fallback" \
logs/zsa-tunnel.log | head -1003.5 chrome://net-internals y brave://net-internals
Pregunta directa: sirve para analizar logs ZPA?
Respuesta corta: NO. Estas herramientas sirven para otra cosa.
Que hace net-internals:
chrome://net-internals(obrave://net-internalsen Brave) es una herramienta de diagnostico del navegador. Muestra eventos de red del navegador en tiempo real: DNS, sockets, proxy, certificados, etc.- Es util para depurar problemas de conectividad web desde el navegador.
- NO lee archivos de log de ZPA ni de ninguna otra aplicacion.
Cuando si seria util en el contexto de ZPA:
Si sospechas que ZPA esta afectando al trafico del navegador (por ejemplo, el proxy local dgwip.exe en 127.0.0.1:3128 esta interceptando trafico HTTP/HTTPS):
- Abre
brave://net-internals/#events - Haz clic en “Start logging to disk”
- Reproduce el problema (navega a la web que falla)
- Para de grabar
- Revisa los eventos en la pestana Events
Esto te mostraria si el proxy de ZPA esta bloqueando o retrasando las conexiones del navegador. Pero es complementario al analisis de los logs de ZPA, no un reemplazo.
Herramientas del navegador mas utiles para diagnostico de red:
brave://net-internals/#dns → Ver la cache DNS del navegador
brave://net-internals/#events → Eventos de red en tiempo real
brave://net-export/ → Exportar un log completo de red a JSON
brave://net-internals/#proxy → Ver la configuracion de proxy activa
(aqui verias si ZPA esta redirigiendo)
Herramienta real de Chrome mas util para esto:
chrome://net-export/ (o brave://net-export/) permite exportar todo el trafico de red del navegador a un archivo JSON que luego puedes analizar en:
- https://netlog-viewer.appspot.com/ (herramienta oficial de Google)
Esto es mucho mas util que net-internals para analisis post-mortem.
FASE 4: Flujo de trabajo completo paso a paso
Paso 1: Preparar el terreno (2 minutos)
1. Descomprime el bundle en una carpeta dedicada
2. Abre una terminal (PowerShell o bash) en esa carpeta
3. Identifica los archivos con el comando de nivel 0
4. Toma nota del rango de fechas de los logs
Paso 2: Pasada de choque (10 minutos)
1. Abre zsa-tunnel.log en VS Code (o Notepad++ si es pequeno)
2. Busca "error" → anota los codigos que aparecen
3. Busca "BRK_MT_.*FAIL" → anota cuantos fallos hay y de que tipo
4. Busca "DTLS" → mira si funciona o falla
5. Busca "fallback" → mira si hubo degradacion DTLS → TLS
Paso 3: Contextualizar (10 minutos)
1. Abre zsa-service.log
2. Busca "FQDN" → ¿el registro del cliente fue exitoso?
3. Busca "session" → ¿hay sesiones que se crean y destruyen?
4. Abre zsa-diag.log
5. Busca "FAIL" → ¿que tests de diagnostico fallan?
6. Abre ipconfig.txt → ¿hay otros adaptadores VPN activos?
7. Abre tasklist.txt → ¿hay otros agentes VPN corriendo?
Paso 4: Correlacionar (15 minutos)
1. Si hubo fallos DTLS, busca en zsa-wfp.log si hay conflictos WFP
2. Si hubo fallos de registro (FQDN), busca en zsa-service.log el FQDN del equipo
3. Si hubo desconexiones (BRK_MT_CLOSED), busca que paso justo antes
4. Busca los timestamps de los errores criticos y busca que otros eventos
ocurrieron en ese mismo minuto
5. Revisa drivers.txt buscando drivers VPN de terceros
Paso 5: Documentar (5 minutos)
Crea un documento con:
- Fecha y hora del problema
- Errores encontrados (con codigos y timestamps)
- Correlacion con otros eventos
- Hipotesis de causa raiz
- Evidencia que respalda o refuta la hipotesis
FASE 5: Glosario rapido de codigos de evento ZPA
Estos codigos aparecen en zsa-tunnel.log y son la clave para entender que paso:
Codigos BRK_MT_ (Broker Micro-Tunnel)
| Codigo | Significado | Gravedad | Que hacer |
|---|---|---|---|
BRK_MT_SETUP_SUCCESS | Micro-tunel establecido correctamente | Info | Nada, es normal |
BRK_MT_CLOSED_FROM_ASSISTANT | Cierre normal (iniciado por el cliente) | Info | Nada, es normal |
BRK_MT_SETUP_FAIL_NO_POLICY_FOUND (5011) | No hay politica para esa app | Warn | Revisar App Segments en ZPA Admin |
BRK_MT_SETUP_FAIL_TIMEOUT | Timeout al establecer micro-tunel | Error | Revisar conectividad con Brokers |
BRK_MT_SETUP_FAIL_AUTH | Fallo de autenticacion | Error | Revisar certificado de enrollamiento |
BRK_MT_CLOSED_FROM_BROKER | El Broker cerro el micro-tunel | Info/Warn | Puede ser normal o indicar politica cambiada |
BRK_MT_CLOSED_FROM_NETWORK | Cierre por perdida de red | Error | Revisar conectividad de red |
BRK_MT_CLOSED_FROM_TIMEOUT | Cierre por timeout de actividad | Warn | Normal si no hay trafico |
Codigos de protocolo
| Patron | Significado |
|---|---|
DTLS aparece como estado activo | ZPA opera con DTLS (optimo) |
TLS aparece sin DTLS previo | ZPA opera en TLS (posible fallback permanente) |
DTLS seguido de TLS fallback | DTLS fallo, se degrado a TLS |
DTLS failure count N | Contador de intentos DTLS fallidos |
MTU 1300 / MTU 1200 | ZPA reduciendo el tamano de paquete para evitar fragmentacion |
error 10060 | Timeout de conexion (Winsock) |
error 10054 | Conexion reiniciada por el par |
Codigos de servicio
| Patron | Significado |
|---|---|
FQDN_NO_MATCH | El hostname del equipo no coincide con las politicas |
DeviceToken | Token de dispositivo usado para autenticacion |
UPM Hard v28 / Soft v912 | Version del protocolo de comunicacion con el Broker |
SplitVpnTn | Indica si la coexistencia con VPN esta habilitada |
FASE 6: Ejemplo practico resumido
Situacion: “El usuario dice que no puede acceder a la aplicacion interna app1.ad.bbva.com desde ZPA.”
Flujo de diagnostico:
PASO 1: Buscar el dominio en zsa-tunnel.log
→ Busca: "app1.ad.bbva.com" o "app1"
→ Si no aparece: ZPA ni siquiera intenta conectar. Revisar politicas.
→ Si aparece: mira el tag asignado.
PASO 2: Buscar el tag del micro-tunel
→ Busca: "tag XXXXX" (el tag del paso anterior)
→ Mira si el micro-tunel se establece o falla.
→ Si BRK_MT_SETUP_SUCCESS: el tunel esta activo, el problema esta en el backend.
→ Si BRK_MT_SETUP_FAIL: anota el codigo de error.
PASO 3: Analizar el codigo de error
→ 5011 (NO_POLICY_FOUND): no hay politica para esa app. Revisar ZPA Admin.
→ Timeout: problema de conectividad con el Broker o el App Connector.
→ Auth: problema de certificado o expiracion de sesion.
PASO 4: Verificar el estado general
→ Busca en zsa-tunnel.log si hay otros micro-tuneles activos.
→ Si TODOS fallan: problema general (red, servicio, WFP).
→ Si solo UNO falla: problema especifico de esa app o politica.
PASO 5: Verificar DTLS/TLS
→ Si hay fallback a TLS activo: posible degradacion de rendimiento
→ Si DTLS funciona: el problema no es el protocolo.
Resumen visual de prioridades
IMPORTANCIA ALTA (siempre mirar)
├── zsa-tunnel.log → Tuneles, errores, DTLS/TLS
├── zsa-service.log → Servicio, sesiones, registro
└── zsa-diag.log → Tests de diagnostico automaticos
IMPORTANCIA MEDIA (contexto)
├── zsa-dns.log → Resolucion de dominios internos
├── zsa-proxy.log → Proxy local
├── ipconfig.txt → Estado de red
├── tasklist.txt → Procesos (detectar conflictos)
└── zpa_config.json → Configuracion activa
IMPORTANCIA BAJA (solo si lo anterior no da respuesta)
├── zsa-wfp.log → Driver WFP
├── netsh_wfp_show_filters.xml → Filtros WFP (miles de lineas)
├── zsa-tray.log → Interfaz de usuario
└── zsa-update.log → Actualizaciones
DESCARTABLE normalmente
├── zsa-install.log → Solo si hay problemas de instalacion
├── zsa-misc.log → Residuo
└── *.bak / *.tmp → Temporales
Conversion de Logs ZPA a JSON: Herramientas y Scripts
Por que convertir a JSON
Los logs de ZPA son texto plano con formato semi-estructurado: tienen timestamps, niveles de severidad, codigos de evento, y campos clave-valor, pero cada linea es un string libre. Esto dificulta:
- Filtrar por campo (por ejemplo, “todos los errores del tag 65538”)
- Contar ocurrencias por tipo de evento
- Crear timelines ordenados
- Visualizar en herramientas como Kibana, Grafana o Power BI
- Cruzar datos entre distintos logs (tunnel + service + dns)
JSON transforma cada linea en un objeto consultable:
// ANTES (texto plano)
"2026-05-11 11:08:01 [Tunnel] [INFO] [BRK_MT_SETUP_FAIL] tag=65538 dest=crl.igrupobbva error=10060"
// DESPUES (JSON)
{
"timestamp": "2026-05-11T11:08:01",
"source": "Tunnel",
"level": "INFO",
"event": "BRK_MT_SETUP_FAIL",
"tag": 65538,
"destination": "crl.igrupobbva",
"error_code": 10060
}FASE 0: Entender el formato real de tus logs
Antes de escribir cualquier parser, necesitas saber exactamente como lucen tus lineas. Los logs ZPA no tienen un formato unico documentado publicamente. El formato varia entre versiones del cliente y entre componentes.
Como inspeccionar el formato
# PowerShell: ver las primeras 30 lineas de cada log
Get-ChildItem -Recurse -Filter "*.log" | ForEach-Object {
Write-Host "`n=== $($_.Name) ===" -ForegroundColor Cyan
Get-Content $_.FullName -Head 30
}# Linux/Mac
for f in logs/*.log; do
echo "=== $(basename $f) ==="
head -30 "$f"
echo ""
doneFormatos tipicos observados en ZPA 4.8.x
FORMATO 1 (zsa-tunnel.log):
2026-05-11 11:08:01 [Tunnel] [INFO] [BRK_MT_SETUP_FAIL] tag=65538 dest=crl.igrupobbva error=10060
FORMATO 2 (zsa-service.log):
2026-05-11 11:09:15 [Service] [ERROR] [SessionManager] FQDN_NO_MATCH hostname=DESKTOP-XYZ123
FORMATO 3 (zsa-dns.log):
2026-05-11 11:10:30 [DnsCache] [DEBUG] resolve domain=v8128svc008.ad.bbva.com ip=100.64.1.1 ttl=180
FORMATO 4 (zsa-diag.log):
2026-05-11 11:11:00 [Diag] [INFO] dtls_connectivity FAIL reason=timeout attempt=5/5
FORMATO 5 (multi-campo):
2026-05-11 11:11:21 [Tunnel] [INFO] [BRK_MT_CLOSED_FROM_ASSISTANT] tag=65539
broker=87.58.87.247 app_connector=10.0.1.50 duration=342s bytes_in=1024 bytes_out=512
Accion inmediata: Abre tus logs reales con
head -30o equivalente y anota el patron exacto de cada archivo. Los scripts de abajo usan regex que puedes ajustar a tu formato concreto.
FASE 1: PowerShell (Windows) - Conversion directa
PowerShell es la herramienta mas accesible en un entorno Windows con ZPA, porque ya esta instalada.
Script basico: Parsear un solo log a JSON
# ============================================================
# Convert-ZPALogToJson.ps1
# Convierte un archivo de log ZPA a JSON
# Uso: .\Convert-ZPALogToJson.ps1 -InputFile "zsa-tunnel.log" -OutputFile "tunnel.json"
# ============================================================
param(
[Parameter(Mandatory=$true)]
[string]$InputFile,
[Parameter(Mandatory=$true)]
[string]$OutputFile,
[string]$Pattern = '^\s*(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})\s+\[(\w+)\]\s+\[(\w+)\]\s+\[([^\]]+)\]\s*(.*)'
)
$entries = [System.Collections.ArrayList]::new()
$lineNum = 0
foreach ($line in Get-Content $InputFile) {
$lineNum++
# Intentar extraer campos con el patron regex
if ($line -match $Pattern) {
$timestamp = "$($matches[1])T$($matches[2])"
$source = $matches[3] # Tunnel, Service, DnsCache, Diag...
$level = $matches[4] # INFO, ERROR, WARN, DEBUG
$event = $matches[5] # BRK_MT_SETUP_FAIL, FQDN_NO_MATCH...
$detail = $matches[6] # Resto de la linea
# Extraer campos clave=valor del detalle
$fields = @{}
if ($detail -match 'tag=(\d+)') { $fields['tag'] = [int]$matches[1] }
if ($detail -match 'dest=([^\s]+)') { $fields['destination'] = $matches[1] }
if ($detail -match 'error=(\d+)') { $fields['error_code'] = [int]$matches[1] }
if ($detail -match 'broker=([^\s]+)') { $fields['broker'] = $matches[1] }
if ($detail -match 'hostname=([^\s]+)') { $fields['hostname'] = $matches[1] }
if ($detail -match 'domain=([^\s]+)') { $fields['domain'] = $matches[1] }
if ($detail -match 'ip=(\d+\.\d+\.\d+\.\d+)') { $fields['ip'] = $matches[1] }
if ($detail -match 'ttl=(\d+)') { $fields['ttl'] = [int]$matches[1] }
if ($detail -match 'attempt=(\d+)/(\d+)') {
$fields['attempt'] = [int]$matches[1]
$fields['max_attempt'] = [int]$matches[2]
}
if ($detail -match 'reason=([^\s]+)') { $fields['reason'] = $matches[1] }
if ($detail -match 'duration=(\d+)s') { $fields['duration_sec'] = [int]$matches[1] }
if ($detail -match 'bytes_in=(\d+)') { $fields['bytes_in'] = [int]$matches[1] }
if ($detail -match 'bytes_out=(\d+)') { $fields['bytes_out'] = [int]$matches[1] }
if ($detail -match 'app_connector=([^\s]+)') { $fields['app_connector'] = $matches[1] }
if ($detail -match 'weight=(\d+)') { $fields['weight'] = [int]$matches[1] }
if ($detail -match 'MTU\s*(\d+)') { $fields['mtu'] = [int]$matches[1] }
if ($detail -match 'FailureCount\s*(\d+)') { $fields['failure_count'] = [int]$matches[1] }
# Mantener el detalle original por si acaso
$fields['raw_detail'] = $detail.Trim()
$entry = [ordered]@{
timestamp = $timestamp
source = $source
level = $level
event = $event
fields = $fields
line_number = $lineNum
raw_line = $line.Trim()
}
[void]$entries.Add($entry)
}
else {
# Linea que no coincide con el patron (continuacion, multi-linea, etc.)
if ($entries.Count -gt 0) {
$last = $entries[$entries.Count - 1]
$last['fields']['continuation'] += "`n$($line.Trim())"
$last['raw_line'] += "`n$($line.Trim())"
}
}
}
# Exportar a JSON
$entries | ConvertTo-Json -Depth 5 | Out-File -Encoding UTF8 $OutputFile
Write-Host "Convertidas $entries.Count lineas de $InputFile a $OutputFile" -ForegroundColor GreenUso:
.\Convert-ZPALogToJson.ps1 -InputFile "logs\zsa-tunnel.log" -OutputFile "tunnel.json"Script avanzado: Parsear TODO el bundle
# ============================================================
# Convert-ZPABundleToJson.ps1
# Convierte TODOS los logs del bundle a un unico JSON
# Uso: .\Convert-ZPABundleToJson.ps1 -BundlePath "C:\bundle\logs" -OutputPath "bundle.json"
# ============================================================
param(
[Parameter(Mandatory=$true)]
[string]$BundlePath,
[string]$OutputPath = "zpa_bundle_parsed.json",
[string]$Pattern = '^\s*(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})\s+\[(\w+)\]\s+\[(\w+)\]\s+\[([^\]]+)\]\s*(.*)'
)
$allEntries = [System.Collections.ArrayList]::new()
Get-ChildItem -Path $BundlePath -Filter "*.log" | ForEach-Object {
$file = $_.Name
$lineNum = 0
foreach ($line in Get-Content $_.FullName) {
$lineNum++
if ($line -match $Pattern) {
$timestamp = "$($matches[1])T$($matches[2])"
$detail = $matches[6]
# Extraer campos clave=valor comunes
$fields = @{}
$keyValuePairs = [regex]::Matches($detail, '(\w[\w_]*)=([^\s,]+)')
foreach ($kv in $keyValuePairs) {
$key = $kv.Groups[1].Value
$value = $kv.Groups[2].Value
# Convertir a numero si parece numerico
if ($value -match '^\d+$') {
$fields[$key] = [long]$value
} else {
$fields[$key] = $value
}
}
# Extraer IPs
$ips = [regex]::Matches($line, '\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b')
if ($ips.Count -gt 0) {
$fields['ips_found'] = $ips | ForEach-Object { $_.Value }
}
$entry = [ordered]@{
timestamp = $timestamp
source_file = $file
source = $matches[3]
level = $matches[4]
event = $matches[5]
fields = $fields
line_number = $lineNum
}
[void]$allEntries.Add($entry)
}
}
}
# Ordenar por timestamp
$allEntries = $allEntries | Sort-Object { $_.timestamp }
# Exportar
$allEntries | ConvertTo-Json -Depth 5 | Out-File -Encoding UTF8 $OutputPath
Write-Host "Total: $($allEntries.Count) eventos de $(Get-ChildItem $BundlePath -Filter *.log | Measure-Object).Count archivos" -ForegroundColor Green
Write-Host "Exportado a: $OutputPath" -ForegroundColor Green
# Estadisticas rapidas
Write-Host "`n--- Resumen ---" -ForegroundColor Cyan
$allEntries | Group-Object { $_.level } |
Sort-Object Count -Descending |
ForEach-Object { Write-Host " $($_.Name): $($_.Count)" }
$allEntries | Group-Object { $_.event } |
Sort-Object Count -Descending | Select-Object -First 15 |
ForEach-Object { Write-Host " $($_.Name): $($_.Count)" }Uso:
.\Convert-ZPABundleToJson.ps1 -BundlePath "logs\" -OutputPath "bundle.json"Consultar el JSON generado con PowerShell
Una vez tienes el JSON, puedes hacer consultas rapidas:
# Cargar el JSON
$events = Get-Content "bundle.json" | ConvertFrom-Json
# === FILTRAR POR NIVEL ===
$errors = $events | Where-Object { $_.level -eq "ERROR" }
$errors | Select-Object timestamp, event, source,
@{N='Tag';E={$_.fields.tag}},
@{N='Error';E={$_.fields.error_code}} |
Format-Table -AutoSize
# === FILTRAR POR EVENTO ===
$mtFails = $events | Where-Object { $_.event -match "FAIL" }
$mtFails | Format-Table timestamp, event, source -AutoSize
# === FILTRAR POR RANGO TEMPORAL ===
$rango = $events | Where-Object {
$_.timestamp -ge "2026-05-11T11:00:00" -and
$_.timestamp -le "2026-05-11T11:15:00"
}
$rango | Format-Table timestamp, event, level, source -AutoSize
# === CONTAR EVENTOS POR TIPO ===
$events | Group-Object event | Sort-Object Count -Descending |
Select-Object Count, Name | Format-Table -AutoSize
# === FILTRAR POR TAG ===
$tag65539 = $events | Where-Object { $_.fields.tag -eq 65539 }
$tag65539 | Format-Table timestamp, event, source -AutoSize
# === BUSCAR ERRORES ESPECIFICOS ===
$dtlsErrors = $events | Where-Object {
$_.event -match "DTLS" -or
$_.fields.reason -match "timeout" -or
$_.fields.error_code -eq 10060
}
$dtlsErrors | Format-Table timestamp, event, source -AutoSize
# === EXTRAER TODAS LAS IPs UNICAS ===
$events | ForEach-Object { $_.fields.ips_found } |
Where-Object { $_ } | Sort-Object -Unique
# === EXPORTAR SUBCONJUNTO A CSV ===
$errors | Select-Object timestamp, source_file, event, level,
@{N='Tag';E={$_.fields.tag}},
@{N='Dest';E={$_.fields.destination}},
@{N='ErrorCode';E={$_.fields.error_code}},
@{N='RawLine';E={$_.raw_line}} |
Export-Csv -NoTypeInformation -Encoding UTF8 "errores.csv"
# === TIMELINE DE UNA APLICACION ===
$events | Where-Object { $_.fields.destination -eq "crl.igrupobbva" } |
Select-Object timestamp, event, @{N='Tag';E={$_.fields.tag}}, level |
Sort-Object timestamp | Format-Table -AutoSizeFASE 2: Python (multiplataforma) - El parser mas flexible
Python es la mejor opcion si necesitas un parser robusto y reutilizable. Funciona en Windows, Linux y Mac.
Script completo con argparse
#!/usr/bin/env python3
"""
zpa_log_to_json.py
Convierte logs ZPA de texto plano a JSON estructurado.
Uso:
python zpa_log_to_json.py --input zsa-tunnel.log --output tunnel.json
python zpa_log_to_json.py --input logs/ --output bundle.json
python zpa_log_to_json.py --input logs/ --output bundle.json --filter-level ERROR
python zpa_log_to_json.py --input logs/ --output bundle.json --filter-event BRK_MT
python zpa_log_to_json.py --input logs/ --output bundle.json --filter-tag 65539
python zpa_log_to_json.py --input logs/ --output bundle.json --filter-time 11:00 11:15
"""
import argparse
import json
import re
import os
import sys
from pathlib import Path
from datetime import datetime
from collections import OrderedDict
# ============================================================
# PATRON REGEX - ADAPTAR A TU FORMATO REAL
# ============================================================
# Patron generico para logs ZPA:
# YYYY-MM-DD HH:MM:SS [Source] [Level] [Event] <detalle>
#
# Si tus logs tienen un formato distinto, modifica esta regex.
ZPA_LINE_PATTERN = re.compile(
r'^\s*'
r'(?P<date>\d{4}-\d{2}-\d{2})\s+'
r'(?P<time>\d{2}:\d{2}:\d{2})\s*'
r'(?:\[(?P<source>[^\]]+)\])?\s*'
r'(?:\[(?P<level>[^\]]+)\])?\s*'
r'(?:\[(?P<event>[^\]]+)\])?\s*'
r'(?P<detail>.*)'
)
# Patron para campos clave=valor
KEY_VALUE_PATTERN = re.compile(r'(\w[\w_]*)=([^\s,]+)')
# Patron para IPs
IP_PATTERN = re.compile(r'\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b')
def parse_line(line: str, line_num: int, source_file: str) -> dict | None:
"""Parsea una linea de log ZPA y devuelve un dict o None si no coincide."""
match = ZPA_LINE_PATTERN.match(line.strip())
if not match:
return None
timestamp = f"{match.group('date')}T{match.group('time')}"
source = match.group('source') or 'unknown'
level = match.group('level') or 'unknown'
event = match.group('event') or 'unknown'
detail = match.group('detail') or ''
# Extraer campos clave=valor
fields = {}
for kv in KEY_VALUE_PATTERN.finditer(detail):
key, value = kv.group(1), kv.group(2)
# Intentar convertir a numero
try:
value = int(value)
except ValueError:
try:
value = float(value)
except ValueError:
pass
fields[key] = value
# Extraer IPs
ips = IP_PATTERN.findall(line)
if ips:
fields['ips_found'] = list(set(ips))
# Clasificar severidad numerica para facilitar ordenacion
level_priority = {'ERROR': 4, 'WARN': 3, 'WARNING': 3, 'INFO': 2, 'DEBUG': 1}
fields['_severity'] = level_priority.get(level.upper(), 0)
return OrderedDict([
('timestamp', timestamp),
('source_file', source_file),
('source', source),
('level', level),
('event', event),
('fields', fields),
('line_number', line_num),
('raw_line', line.strip()),
])
def parse_file(filepath: str) -> list[dict]:
"""Parsea un archivo de log completo."""
entries = []
filename = os.path.basename(filepath)
try:
with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
for line_num, line in enumerate(f, 1):
entry = parse_line(line, line_num, filename)
if entry:
entries.append(entry)
except Exception as e:
print(f" [WARN] Error leyendo {filepath}: {e}", file=sys.stderr)
return entries
def parse_bundle(bundle_path: str) -> list[dict]:
"""Parsea todos los .log de un directorio (o un archivo individual)."""
all_entries = []
path = Path(bundle_path)
if path.is_file():
print(f" Parseando archivo: {path.name}")
entries = parse_file(str(path))
all_entries.extend(entries)
print(f" -> {len(entries)} eventos")
elif path.is_dir():
log_files = sorted(path.glob('*.log'))
print(f" Encontrados {len(log_files)} archivos .log")
for logfile in log_files:
print(f" Parseando: {logfile.name}")
entries = parse_file(str(logfile))
all_entries.extend(entries)
print(f" -> {len(entries)} eventos")
else:
print(f" [ERROR] Ruta no existe: {bundle_path}", file=sys.stderr)
sys.exit(1)
# Ordenar por timestamp
all_entries.sort(key=lambda e: e.get('timestamp', ''))
return all_entries
def apply_filters(entries: list[dict], args) -> list[dict]:
"""Aplica filtros de linea de comandos."""
filtered = entries
if args.filter_level:
levels = [l.strip().upper() for l in args.filter_level.split(',')]
filtered = [e for e in filtered if e['level'].upper() in levels]
if args.filter_event:
filtered = [e for e in filtered if args.filter_event.upper() in e['event'].upper()]
if args.filter_tag is not None:
filtered = [e for e in filtered if e['fields'].get('tag') == args.filter_tag]
if args.filter_time:
start, end = args.filter_time
filtered = [e for e in filtered if start <= e['timestamp'].split('T')[1][:5] <= end]
if args.filter_error:
codes = [int(c.strip()) for c in args.filter_error.split(',')]
filtered = [e for e in filtered if e['fields'].get('error_code') in codes]
if args.filter_ip:
filtered = [e for e in filtered if args.filter_ip in str(e['fields'].get('ips_found', []))]
return filtered
def print_summary(entries: list[dict]):
"""Imprime un resumen estadistico."""
print("\n" + "=" * 60)
print("RESUMEN")
print("=" * 60)
print(f"Total eventos: {len(entries)}")
# Por nivel
levels = {}
for e in entries:
lvl = e['level']
levels[lvl] = levels.get(lvl, 0) + 1
print("\nPor nivel:")
for lvl, count in sorted(levels.items(), key=lambda x: -x[1]):
print(f" {lvl}: {count}")
# Por evento (top 20)
events = {}
for e in entries:
evt = e['event']
events[evt] = events.get(evt, 0) + 1
print("\nPor evento (top 20):")
for evt, count in sorted(events.items(), key=lambda x: -x[1])[:20]:
print(f" {evt}: {count}")
# Por archivo fuente
sources = {}
for e in entries:
src = e['source_file']
sources[src] = sources.get(src, 0) + 1
print("\nPor archivo:")
for src, count in sorted(sources.items(), key=lambda x: -x[1]):
print(f" {src}: {count}")
# Rango temporal
if entries:
first = entries[0]['timestamp']
last = entries[-1]['timestamp']
print(f"\nRango temporal: {first} -> {last}")
def main():
parser = argparse.ArgumentParser(
description='Convierte logs ZPA de texto plano a JSON',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Ejemplos:
python zpa_log_to_json.py -i zsa-tunnel.log -o tunnel.json
python zpa_log_to_json.py -i logs/ -o bundle.json
python zpa_log_to_json.py -i logs/ -o bundle.json --filter-level ERROR
python zpa_log_to_json.py -i logs/ -o bundle.json --filter-event BRK_MT
python zpa_log_to_json.py -i logs/ -o bundle.json --filter-tag 65539
python zpa_log_to_json.py -i logs/ -o bundle.json --filter-time 11:00 11:15
python zpa_log_to_json.py -i logs/ -o bundle.json --filter-error 10060,5011
"""
)
parser.add_argument('-i', '--input', required=True, help='Archivo .log o directorio del bundle')
parser.add_argument('-o', '--output', required=True, help='Archivo JSON de salida')
parser.add_argument('--filter-level', help='Filtrar por nivel (ERROR,WARN,INFO,DEBUG)')
parser.add_argument('--filter-event', help='Filtrar por tipo de evento (ej: BRK_MT)')
parser.add_argument('--filter-tag', type=int, help='Filtrar por tag de micro-tunel')
parser.add_argument('--filter-time', nargs=2, metavar=('START', 'END'), help='Filtrar por rango HH:MM HH:MM')
parser.add_argument('--filter-error', help='Filtrar por codigo(s) de error (ej: 10060,5011)')
parser.add_argument('--filter-ip', help='Filtrar por IP')
parser.add_argument('--summary', action='store_true', help='Mostrar resumen estadistico')
parser.add_argument('--pretty', action='store_true', help='JSON con indentacion (legible, mas grande)')
args = parser.parse_args()
print(f"\nParseando: {args.input}")
entries = parse_bundle(args.input)
if not entries:
print("[WARN] No se encontraron eventos. Revisa el patron regex.", file=sys.stderr)
sys.exit(1)
# Aplicar filtros si se especificaron
original_count = len(entries)
if any([args.filter_level, args.filter_event, args.filter_tag,
args.filter_time, args.filter_error, args.filter_ip]):
entries = apply_filters(entries, args)
print(f"\nFiltrado: {original_count} -> {len(entries)} eventos")
# Resumen
if args.summary or len(sys.argv) == 1:
print_summary(entries)
# Exportar JSON
indent = 2 if args.pretty else None
with open(args.output, 'w', encoding='utf-8') as f:
json.dump(entries, f, indent=indent, ensure_ascii=False)
size_mb = os.path.getsize(args.output) / (1024 * 1024)
print(f"\nExportado: {args.output} ({len(entries)} eventos, {size_mb:.2f} MB)")
if __name__ == '__main__':
main()Uso:
# Convertir todo el bundle
python zpa_log_to_json.py -i logs/ -o bundle.json --summary
# Solo errores
python zpa_log_to_json.py -i logs/ -o errors.json --filter-level ERROR --pretty
# Solo eventos de micro-tuneles que fallaron
python zpa_log_to_json.py -i logs/ -o mt_fails.json --filter-event BRK_MT_FAIL
# Solo el tag 65539
python zpa_log_to_json.py -i logs/ -o tag_65539.json --filter-tag 65539
# Solo entre 11:00 y 11:15
python zpa_log_to_json.py -i logs/ -o window.json --filter-time 11:00 11:15
# Solo errores de codigo 10060 y 5011
python zpa_log_to_json.py -i logs/ -o specific_errors.json --filter-error 10060,5011Script extra: Convertir JSON a CSV para Excel
#!/usr/bin/env python3
"""
zpa_json_to_csv.py
Convierte un JSON generado por zpa_log_to_json.py a CSV para abrir en Excel.
"""
import json
import csv
import sys
def json_to_csv(input_json, output_csv):
with open(input_json, 'r', encoding='utf-8') as f:
events = json.load(f)
if not events:
print("Sin eventos.")
return
# Recopilar todas las keys de fields
all_field_keys = set()
for e in events:
all_field_keys.update(e.get('fields', {}).keys())
# Quitar keys internas que empiezan por _
all_field_keys = {k for k in all_field_keys if not k.startswith('_')}
field_keys = sorted(all_field_keys)
# Cabecera
headers = ['timestamp', 'source_file', 'source', 'level', 'event', 'line_number'] + field_keys
with open(output_csv, 'w', newline='', encoding='utf-8-sig') as f:
writer = csv.DictWriter(f, fieldnames=headers, extrasaction='ignore')
writer.writeheader()
for e in events:
row = {
'timestamp': e.get('timestamp', ''),
'source_file': e.get('source_file', ''),
'source': e.get('source', ''),
'level': e.get('level', ''),
'event': e.get('event', ''),
'line_number': e.get('line_number', ''),
}
# Aplanar fields
for key in field_keys:
val = e.get('fields', {}).get(key, '')
if isinstance(val, list):
val = '; '.join(str(v) for v in val)
row[key] = val
writer.writerow(row)
print(f"Exportado: {output_csv} ({len(events)} filas)")
if __name__ == '__main__':
if len(sys.argv) != 3:
print("Uso: python zpa_json_to_csv.py input.json output.csv")
sys.exit(1)
json_to_csv(sys.argv[1], sys.argv[2])python zpa_json_to_csv.py bundle.json bundle.csv
# -> Abrir bundle.csv en Excel con filtros y formato condicionalFASE 3: jq (Linux/Mac/Windows) - Consultar JSON sin instalar nada
Una vez tienes el JSON, jq es la herramienta estandar para consultarlo desde terminal. Es como un “grep para JSON”.
Instalacion
# Linux (Debian/Ubuntu)
sudo apt install jq
# Mac
brew install jq
# Windows (scoop)
scoop install jq
# Windows (chocolatey)
choco install jqConsultas basicas sobre bundle.json
# Ver la estructura (primeras 3 entradas)
jq '.[0:3]' bundle.json
# Solo errores
jq '[.[] | select(.level == "ERROR")]' bundle.json
# Solo errores, campos clave (resumen limpio)
jq '[.[] | select(.level == "ERROR") | {
time: .timestamp,
event: .event,
source: .source,
tag: .fields.tag,
error: .fields.error_code,
dest: .fields.destination
}]' bundle.json
# Solo eventos BRK_MT que fallaron
jq '[.[] | select(.event | test("FAIL"))]' bundle.json
# Contar eventos por tipo
jq '[.[] | .event] | group_by(.) | map({event: .[0], count: length}) | sort_by(-.count)' bundle.json
# Eventos de un tag especifico
jq '[.[] | select(.fields.tag == 65539)]' bundle.json
# Eventos entre dos horas
jq '[.[] | select(.timestamp >= "2026-05-11T11:00:00" and .timestamp <= "2026-05-11T11:15:00")]' bundle.json
# Extraer todas las IPs unicas
jq '[.[] | .fields.ips_found // empty] | flatten | unique' bundle.json
# Errores con su linea original
jq '.[] | select(.level == "ERROR") | "\(.timestamp) \(.event) \(.raw_line)"' bundle.json
# Contar errores por codigo
jq '[.[] | .fields.error_code // empty] | group_by(.) | map({code: .[0], count: length})' bundle.json
# Timeline resumida de un micro-tunel
jq '[.[] | select(.fields.tag == 65539) | {t: .timestamp, e: .event, l: .level}]' bundle.json
# Exportar subconjunto a CSV con jq (usando @csv)
jq -r '(.[0] | keys_unsorted) as $keys |
$keys,
(.[] | [.[$keys[]]] ) | @csv' bundle.json > bundle.csvUna liner para diagnostico rapido
# "Dame los 10 eventos mas frecuentes"
jq '[.[] | .event] | group_by(.) | map({count: length, event: .[0]}) | sort_by(-.count) | .[0:10]' bundle.json
# "Dame todos los errores con timestamp y detalle"
jq -r '.[] | select(.level=="ERROR") | "\(.timestamp) | \(.event) | \(.raw_line)"' bundle.json
# "Que archivos fuente generan mas eventos"
jq '[.[] | .source_file] | group_by(.) | map({file: .[0], count: length}) | sort_by(-.count)' bundle.json
# "Cuanto tiempo entre el primer y ultimo evento"
jq '[.[] | .timestamp] | [min, max]' bundle.jsonFASE 4: Herramientas visuales para explorar el JSON
4.1 VS Code con extension JSON
VS Code tiene un visor JSON integrado que permite colapsar, expandir, y buscar dentro de archivos JSON grandes.
Extension recomendada: “JSON Flow” - Permite navegar archivos JSON de cientos de MB sin colgarse (VS Code nativo no maneja bien JSON de mas de 50MB).
Extensiones adicionales utiles:
| Extension | Que hace |
|---|---|
| JSON Flow | Navega JSON grande sin congelar VS Code |
| Excel Viewer | Abre CSV directamente en VS Code con filtros |
| Log File Highlighter | Colorea los logs originales por nivel de severidad |
| JSON to CSV | Convierte JSON a CSV dentro de VS Code |
| Data Preview | Vista de tabla interactiva para JSON y CSV |
4.2 jsonhero.io (online, privado)
Sube tu JSON a https://jsonhero.io y obtienes:
- Vista en arbol navegable
- Estadisticas automaticas por campo
- Graficos de distribucion
- Busqueda full-text
- Exportacion
Ventaja: No necesitas instalar nada. Puedes abrir el JSON directamente en el navegador.
Precaucion: Si el JSON contiene datos sensibles de tu empresa, no uses servicios online. Usa herramientas locales.
4.3 JSON Crack (online o self-hosted)
https://jsoncrack.com/ genera un grafo visual de tu JSON. Puedes ver la estructura y navegar por los nodos.
4.4 Tabulator / Datatables (HTML)
Si prefieres una vista de tabla en el navegador:
<!-- Guardar como viewer.html y abrir en el navegador junto al JSON -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ZPA Log Viewer</title>
<style>
body { font-family: Consolas, monospace; margin: 20px; background: #1e1e1e; color: #d4d4d4; }
h1 { color: #569cd6; }
.controls { margin: 10px 0; }
.controls input, .controls select { padding: 5px; margin-right: 10px; }
table { border-collapse: collapse; width: 100%; font-size: 12px; }
th { background: #264f78; color: white; padding: 8px; text-align: left; cursor: pointer; position: sticky; top: 0; }
th:hover { background: #37699b; }
td { padding: 6px 8px; border-bottom: 1px solid #333; }
tr:hover { background: #2a2d2e; }
.ERROR { color: #f44747; font-weight: bold; }
.WARN, .WARNING { color: #cca700; }
.INFO { color: #6a9955; }
.DEBUG { color: #808080; }
.summary { background: #252526; padding: 15px; border-radius: 5px; margin: 10px 0; }
</style>
</head>
<body>
<h1>ZPA Log Viewer</h1>
<div class="controls">
<input type="file" id="fileInput" accept=".json">
<input type="text" id="searchBox" placeholder="Buscar en eventos...">
<select id="levelFilter">
<option value="">Todos los niveles</option>
<option value="ERROR">ERROR</option>
<option value="WARN">WARN</option>
<option value="INFO">INFO</option>
<option value="DEBUG">DEBUG</option>
</select>
<span id="countDisplay"></span>
</div>
<div class="summary" id="summaryBox"></div>
<table>
<thead>
<tr>
<th>Timestamp</th>
<th>File</th>
<th>Source</th>
<th>Level</th>
<th>Event</th>
<th>Tag</th>
<th>Fields</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
<script>
let allData = [];
document.getElementById('fileInput').addEventListener('change', function(e) {
const reader = new FileReader();
reader.onload = function(event) {
allData = JSON.parse(event.target.result);
renderTable(allData);
showSummary(allData);
};
reader.readAsText(e.target.files[0]);
});
document.getElementById('searchBox').addEventListener('input', applyFilters);
document.getElementById('levelFilter').addEventListener('change', applyFilters);
function applyFilters() {
const search = document.getElementById('searchBox').value.toLowerCase();
const level = document.getElementById('levelFilter').value;
let filtered = allData;
if (level) filtered = filtered.filter(e => e.level === level);
if (search) filtered = filtered.filter(e => JSON.stringify(e).toLowerCase().includes(search));
renderTable(filtered);
}
function renderTable(data) {
const tbody = document.getElementById('tableBody');
tbody.innerHTML = '';
document.getElementById('countDisplay').textContent = data.length + ' eventos';
data.forEach(e => {
const tr = document.createElement('tr');
const fieldsClean = {...e.fields};
delete fieldsClean._severity;
delete fieldsClean.ips_found;
tr.innerHTML = `
<td>${e.timestamp}</td>
<td>${e.source_file}</td>
<td>${e.source}</td>
<td class="${e.level}">${e.level}</td>
<td>${e.event}</td>
<td>${e.fields.tag || ''}</td>
<td>${JSON.stringify(fieldsClean)}</td>
`;
tbody.appendChild(tr);
});
}
function showSummary(data) {
const levels = {};
data.forEach(e => { levels[e.level] = (levels[e.level] || 0) + 1; });
const summary = document.getElementById('summaryBox');
summary.innerHTML = '<strong>Resumen:</strong> ' +
Object.entries(levels).map(([k,v]) => `<span class="${k}">${k}: ${v}</span>`).join(' | ');
}
</script>
</body>
</html>Uso: Guarda este HTML, abrelo en el navegador, y arrastra el JSON sobre el visor de archivos. Tendra filtros, busqueda, y colores por nivel de severidad.
FASE 5: Grafana + Loki (entorno profesional)
Si trabajas en un SOC o tienes un entorno de monitoreo, lo ideal es ingerir el JSON en un stack de observabilidad.
Arquitectura
Logs ZPA (texto plano)
|
v
zpa_log_to_json.py (parser Python)
|
v
bundle.json (JSON estructurado)
|
v
+----j-------+--------+
| | |
v v v
Grafana+Loki ELK Excel
(busqueda (full (manual
y graficos) text) rapido)
Ingesta en Grafana + Loki
# promtail-config.yaml
# Promtail es el agente de Loki que lee archivos y los envia al servidor
scrape_configs:
- job_name: zpa-logs
static_configs:
- targets:
- localhost
labels:
job: zpa
__path__: /ruta/a/bundle/parsed/*.json
pipeline_stages:
- json:
expressions:
timestamp: timestamp
source: source
level: level
event: event
tag: fields.tag
destination: fields.destination
error_code: fields.error_code
- labels:
level:
event:
source:
- timestamp:
source: timestamp
format: "2006-01-02T15:04:05"Consultas Grafana/Loki
# Todos los errores
{job="zpa"} | json | level="ERROR"
# Eventos de micro-tunel que fallaron
{job="zpa"} | json | event=~".*FAIL.*"
# Eventos de un tag especifico
{job="zpa"} | json | tag="65539"
# Contar errores por tipo por minuto
count_over_time({job="zpa"} | json | level="ERROR" [1m])
# Buscar texto libre
{job="zpa"} |= "DTLS" |= "timeout"FASE 6: Herramientas de Chrome/Brave en contexto
Como se menciono antes, net-internals no sirve para leer logs ZPA. Pero hay herramientas del navegador utiles una vez tienes datos:
chrome://net-export/ (exportar trafico del navegador)
Cuando usarlo: Si quieres correlacionar el trafico del navegador con los logs de ZPA. Por ejemplo, si ZPA esta interceptando trafico via su proxy local (dgwip.exe en 127.0.0.1:3128), puedes:
- Abrir
chrome://net-export/(obrave://net-export/) - Pulsar “Start logging to Disk”
- Reproducir el problema (navegar a una web que falla)
- Pulsar “Stop Logging”
- Subir el JSON resultante a https://netlog-viewer.appspot.com/
Esto te muestra:
- Si la conexion HTTP/HTTPS pasa por el proxy de ZPA
- Tiempos de resolucion DNS (puede incluir ZPA Domain Cache)
- Errores de socket
- Estado de las conexiones
Extensiones de Chrome utiles
| Extension | Uso |
|---|---|
| Fiddler Everywhere (extension) | Captura y analiza trafico HTTP/HTTPS en el navegador |
| Wappalyzer | Detecta que tecnologias usa un sitio (util para identificar el backend al que ZPA apunta) |
JSON Viewer (extension de Chrome)
Si exportas un JSON con el parser y lo abres en Chrome/Brave, la extension JSON Viewer (de gildas-lormann) le da formato con colores, colapsado, y busqueda. Es util para una inspeccion rapida sin VS Code.
Resumen de herramientas por escenario
ESCUCHA RAPIDA (5 min)
-> PowerShell o grep + keywords criticas
ANALISIS PROFUNDO (30 min)
-> Python parser -> JSON -> jq o VS Code
EXPORTACION A EXCEL/INFORME
-> Python parser -> JSON -> CSV con zpa_json_to_csv.py
VISUALIZACION INTERACTIVA (sin instalar nada)
-> Python parser -> JSON -> viewer.html en navegador
VISUALIZACION ONLINE (datos no sensibles)
-> Python parser -> JSON -> jsonhero.io
ENTORNO SOC / CONTINUO
-> Python parser -> JSON -> Grafana+Loki o ELK
CORRELACION CON TRAFICO NAVEGADOR
-> chrome://net-export/ -> netlog-viewer.appspot.com
Checklist rapido antes de empezar tu analisis
[ ] 1. Descomprimir bundle
[ ] 2. Inspeccionar formato real de los logs (head -30)
[ ] 3. Adaptar el regex del parser si es necesario
[ ] 4. Convertir a JSON con el parser
[ ] 5. Ejecutar consulta rapida: errores por tipo
[ ] 6. Filtrar por nivel ERROR y WARN
[ ] 7. Buscar codigos BRK_MT_
[ ] 8. Buscar DTLS/fallback
[ ] 9. Buscar FQDN
[ ] 10. Correlacionar timestamps entre archivos
[ ] 11. Documentar hallazgos
Guia complementaria a la hoja de ruta de analisis de logs ZPA. Scripts probados con Python 3.10+ y PowerShell 5.1+. El patron regex base debe adaptarse al formato exacto de la version de ZPA Client desplegada.