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 administración), obtienes un ZIP que contiene decenas de archivos de log. Estos logs cubren multiples componentes del cliente y pueden remontarse varios días atras. El volumen puede ser abrumador.
El objetivo no es leer todo. Es encontrar rápidamente donde está el problema.
FASE 1: Conocer la estructura del bundle
Estructura típica de un Support Bundle ZPA
Al descomprimir el ZIP, te encuentras con una carpeta que contiene algo así:
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 versión 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 rápida (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 más grandes son los que más actividad tienen. Si zsa-tunnel.log tiene 50MB y zsa-tray.log tiene 200KB, ya sabes dónde está la acción.
Nivel 1 - Los 3 archivos que siempre miras primero (15 minutos)
En orden de prioridad:
1. zsa-tunnel.log - EL ARCHIVO MÁS IMPORTANTE
Por qué: Aquí se registran las conexiones de los micro-túneles: establecimiento, errores, fallos DTLS, timeouts, desconexiones. Si algo falla en ZPA, se refleja aquí.
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 qué: Gestiona el ciclo de vida del cliente, sesiones de usuario, y eventos del sistema. Si el servicio se cuelga, se reinicia, o pierde la sesión, aquí 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 DIAGNÓSTICO
Por qué: Este archivo contiene los resultados del diagnóstico interno de ZPA. Incluye tests de conectividad, estado de túneles, y problemas detectados automáticamente.
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)
Después 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 | Tráfico 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 | Configuración 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 actualización causo el problema | Baja |
zsa-install.log | Solo si el problema es la instalación 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 más accesible para análisis manual de logs.
Busqueda básica: Ctrl+F → Find in Files
Atajo: Ctrl + Shift + F
Find what: error
Directory: C:\ruta\al\bundle\logs
Filters: *.log
Search Mode: Normal
[x] Match case: NO (primera pasada)
Esto te da todas las líneas 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 líneas largas).
Paso 2: Marcar todas las ocurrencias
Ctrl + F → pestaña “Mark” → escribe la palabra clave → “Mark All”. Esto colorea todas las líneas que coinciden. Luego puedes navegar entre ellas con los botones “Next” y “Previous”.
Paso 3: Extraer solo las líneas que interesan
Search -> Find in Files → marca la opción “Bookmark Line”. Después, Search -> Bookmark -> Copy Bookmarked Lines. Esto copia solo las líneas relevantes a un nuevo documento para análisis 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 automáticamente
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 tamaño sin colgarse
- Busqueda regex rápida en multiples archivos
- Permite colapsar secciones por timestamps
- Tiene extensiones especificas para logs
Extensión recomendada
Instala “Log File Highlighter” de emilast. Automáticamente colorea:
- Líneas con
ERROR/erroren rojo - Líneas con
WARN/warningen amarillo - Líneas 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 análisis rápido
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 más 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 diagnóstico del navegador. Muestra eventos de red del navegador en tiempo real: DNS, sockets, proxy, certificados, etc.- Es útil para depurar problemas de conectividad web desde el navegador.
- NO lee archivos de log de ZPA ni de ninguna otra aplicación.
Cuando si sería útil en el contexto de ZPA:
Si sospechas que ZPA está afectando al tráfico del navegador (por ejemplo, el proxy local dgwip.exe en 127.0.0.1:3128 está interceptando tráfico HTTP/HTTPS):
1. 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 pestaña Events
Esto te mostraria si el proxy de ZPA está bloqueando o retrasando las conexiones del navegador. Pero es complementario al análisis de los logs de ZPA, no un reemplazo.
Herramientas del navegador más útiles para diagnóstico 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 más útil para esto:
chrome://net-export/ (o brave://net-export/) permite exportar todo el tráfico 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 más útil que net-internals para análisis 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 rápido de códigos de evento ZPA
Estos códigos aparecen en zsa-tunnel.log y son la clave para entender que paso:
Códigos BRK_MT_ (Broker Micro-Tunnel)
| Código | Significado | Gravedad | Que hacer |
|---|---|---|---|
BRK_MT_SETUP_SUCCESS | Micro-túnel 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 política para esa app | Warn | Revisar App Segments en ZPA Admin |
BRK_MT_SETUP_FAIL_TIMEOUT | Timeout al establecer micro-túnel | Error | Revisar conectividad con Brokers |
BRK_MT_SETUP_FAIL_AUTH | Fallo de autenticación | Error | Revisar certificado de enrollamiento |
BRK_MT_CLOSED_FROM_BROKER | El Broker cerro el micro-túnel | Info/Warn | Puede ser normal o indicar política 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 tráfico |
Códigos de protocolo
| Patron | Significado |
|---|---|
DTLS aparece como estado activo | ZPA opera con DTLS (óptimo) |
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 tamaño de paquete para evitar fragmentación |
error 10060 | Timeout de conexión (Winsock) |
error 10054 | Conexión reiniciada por el par |
Códigos de servicio
| Patron | Significado |
|---|---|
FQDN_NO_MATCH | El hostname del equipo no coincide con las políticas |
DeviceToken | Token de dispositivo usado para autenticación |
UPM Hard v28 / Soft v912 | Versión del protocolo de comunicación con el Broker |
SplitVpnTn | Indica si la coexistencia con VPN está habilitada |
FASE 6: Ejemplo práctico resumido
Situación: “El usuario dice que no puede acceder a la aplicación interna app1.ad.corp.example.com desde ZPA.”
Flujo de diagnóstico:
PASO 1: Buscar el dominio en zsa-tunnel.log
-> Busca: "app1.ad.corp.example.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
Conversión de Logs ZPA a JSON: Herramientas y Scripts
Por qué convertir a JSON
Los logs de ZPA son texto plano con formato semi-estructurado: tienen timestamps, niveles de severidad, códigos de evento, y campos clave-valor, pero cada línea 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 línea en un objeto consultable:
// ANTES (texto plano)
"2026-05-11 11:08:01 [Tunnel] [INFO] [BRK_MT_SETUP_FAIL] tag=65538 dest=crl.corp-example error=10060"
// DESPUES (JSON)
{
"timestamp": "2026-05-11T11:08:01",
"source": "Tunnel",
"level": "INFO",
"event": "BRK_MT_SETUP_FAIL",
"tag": 65538,
"destination": "crl.corp-example",
"error_code": 10060
}FASE 0: Entender el formato real de tus logs
Antes de escribir cualquier parser, necesitas saber exactamente como lucen tus líneas. Los logs ZPA no tienen un formato único documentado públicamente. 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 típicos 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.corp-example 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=srv-app01.ad.corp.example.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=198.51.100.10 app_connector=10.10.10.50 duration=342s bytes_in=1024 bytes_out=512
Acción 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) - Conversión directa
PowerShell es la herramienta más accesible en un entorno Windows con ZPA, porque ya está instalada.
Script básico: 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.corp-example" } |
Select-Object timestamp, event, @{N='Tag';E={$_.fields.tag}}, level |
Sort-Object timestamp | Format-Table -AutoSizeFASE 2: Python (multiplataforma) - El parser más flexible
Python es la mejor opción 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 condicional
FASE 3: jq (Linux/Mac/Windows) - Consultar JSON sin instalar nada
Una vez tienes el JSON, jq es la herramienta estándar para consultarlo desde terminal. Es como un “grep para JSON”.
Instalación
# 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 diagnóstico rápido
# "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 extensión JSON
VS Code tiene un visor JSON integrado que permite colapsar, expandir, y buscar dentro de archivos JSON grandes.
Extensión recomendada: “JSON Flow” - Permite navegar archivos JSON de cientos de MB sin colgarse (VS Code nativo no maneja bien JSON de más de 50MB).
Extensiones adicionales útiles:
| Extensión | 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 árbol navegable
- Estadisticas automaticas por campo
- Gráficos de distribución
- Busqueda full-text
- Exportación
Ventaja: No necesitas instalar nada. Puedes abrir el JSON directamente en el navegador.
Precaución: 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 útiles una vez tienes datos:
chrome://net-export/ (exportar tráfico del navegador)
Cuando usarlo: Si quieres correlacionar el tráfico del navegador con los logs de ZPA. Por ejemplo, si ZPA está interceptando tráfico via su proxy local (dgwip.exe en 127.0.0.1:3128), puedes:
1. Abrir chrome://net-export/ (o brave://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 conexión HTTP/HTTPS pasa por el proxy de ZPA
- Tiempos de resolución DNS (puede incluir ZPA Domain Cache)
- Errores de socket
- Estado de las conexiones
Extensiones de Chrome útiles
| Extensión | Uso |
|---|---|
| Fiddler Everywhere (extensión) | Captura y analiza tráfico HTTP/HTTPS en el navegador |
| Wappalyzer | Detecta que tecnologías usa un sitio (útil para identificar el backend al que ZPA apunta) |
JSON Viewer (extensión de Chrome)
Si exportas un JSON con el parser y lo abres en Chrome/Brave, la extensión JSON Viewer (de gildas-lormann) le da formato con colores, colapsado, y busqueda. Es útil para una inspección rápida 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 rápido antes de empezar tu análisis
[ ] 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
Guía complementaria a la hoja de ruta de análisis de logs ZPA. Scripts probados con Python 3.10+ y PowerShell 5.1+. El patron regex base debe adaptarse al formato exacto de la versión de ZPA Client desplegada.