--- title: "Análisis de logs con grep, awk y sed en Linux" description: "Aprende a filtrar, contar y resumir logs de Nginx y de SSH en Ubuntu con grep, awk y sed, y a combinarlos en tuberías que responden preguntas concretas." category: "Monitoring Logging" language: es source: https://cubepath.com/es/docs/monitoring-logging/analisis-de-logs-con-awk-grep-y-sed updated: 2026-09-25 --- # Análisis de logs con grep, awk y sed en Linux Cuando algo falla en un servidor, la respuesta suele estar en los logs, y `grep`, `awk` y `sed` permiten encontrarla en segundos sin instalar nada. `grep` selecciona líneas, `awk` trabaja con columnas y hace cálculos, y `sed` transforma o recorta texto. En este tutorial usarás las tres herramientas sobre dos logs habituales en Ubuntu 24.04, el access log de Nginx y `/var/log/auth.log`, para responder preguntas reales: qué IP generan más tráfico, qué URL devuelven errores, cuándo empezó un problema y quién intenta entrar por SSH. ## Requisitos previos Para seguir esta guía necesitas: - Un servidor con Ubuntu 24.04 LTS (o Debian 12) y un usuario con privilegios `sudo`, por ejemplo un VPS de CubePath. - Conocimientos básicos de la terminal y de tuberías (`|`). `grep`, `sed` y `awk` vienen instalados. En Ubuntu, `awk` es `mawk`, rápido y compatible con todos los ejemplos de esta guía. Los logs de sistema de `/var/log` pertenecen al grupo `adm`; léelos con `sudo` o añade tu usuario al grupo con `sudo usermod -aG adm your_user` y vuelve a iniciar sesión. ## Paso 1: Preparar logs de ejemplo Para que puedas comprobar que obtienes los mismos resultados, crea dos archivos de ejemplo en un directorio de trabajo. El primero sigue el formato `combined` de Nginx y Apache: ```bash mkdir -p ~/logs-lab && cd ~/logs-lab cat > access.log <<'EOF' 203.0.113.10 - - [25/Sep/2026:10:01:12 +0000] "GET / HTTP/1.1" 200 5320 "-" "Mozilla/5.0 (X11; Linux x86_64)" 203.0.113.10 - - [25/Sep/2026:10:01:13 +0000] "GET /css/app.css HTTP/1.1" 200 18234 "https://example.com/" "Mozilla/5.0 (X11; Linux x86_64)" 198.51.100.23 - - [25/Sep/2026:10:02:40 +0000] "GET /wp-login.php HTTP/1.1" 404 162 "-" "python-requests/2.31" 198.51.100.23 - - [25/Sep/2026:10:02:41 +0000] "GET /.env HTTP/1.1" 404 162 "-" "python-requests/2.31" 198.51.100.23 - - [25/Sep/2026:10:02:41 +0000] "GET /admin/config.php HTTP/1.1" 404 162 "-" "python-requests/2.31" 192.0.2.55 - - [25/Sep/2026:10:15:03 +0000] "POST /api/orders HTTP/1.1" 201 412 "-" "curl/8.5.0" 192.0.2.55 - - [25/Sep/2026:10:15:09 +0000] "POST /api/orders HTTP/1.1" 502 166 "-" "curl/8.5.0" 203.0.113.10 - - [25/Sep/2026:11:20:44 +0000] "GET /products?id=42 HTTP/1.1" 200 7421 "https://example.com/" "Mozilla/5.0 (X11; Linux x86_64)" 192.0.2.55 - - [25/Sep/2026:11:21:02 +0000] "POST /api/orders HTTP/1.1" 502 166 "-" "curl/8.5.0" 203.0.113.77 - - [25/Sep/2026:11:40:19 +0000] "GET /products?id=7 HTTP/1.1" 200 7390 "-" "Mozilla/5.0 (Macintosh)" EOF ``` El segundo imita `/var/log/auth.log` en Ubuntu 24.04, que usa marcas de tiempo ISO 8601: ```bash cat > auth.log <<'EOF' 2026-09-25T09:58:01.120334+00:00 web01 sshd[2101]: Failed password for root from 198.51.100.23 port 40122 ssh2 2026-09-25T09:58:04.502113+00:00 web01 sshd[2101]: Failed password for root from 198.51.100.23 port 40122 ssh2 2026-09-25T09:59:10.884120+00:00 web01 sshd[2140]: Failed password for invalid user admin from 198.51.100.23 port 40388 ssh2 2026-09-25T10:03:22.004511+00:00 web01 sshd[2188]: Failed password for invalid user oracle from 203.0.113.200 port 51544 ssh2 2026-09-25T10:05:47.310022+00:00 web01 sshd[2203]: Accepted publickey for deploy from 192.0.2.10 port 53120 ssh2: ED25519 SHA256:q1w2e3r4t5y6u7i8o9p0 2026-09-25T10:05:47.412874+00:00 web01 sshd[2203]: pam_unix(sshd:session): session opened for user deploy(uid=1001) by deploy(uid=0) 2026-09-25T10:30:02.000118+00:00 web01 CRON[2301]: pam_unix(cron:session): session opened for user root(uid=0) by root(uid=0) EOF ``` Cuando termines, aplica los mismos comandos a tus logs reales: `/var/log/nginx/access.log` y `/var/log/auth.log`. ### Entender los campos del access log `awk` divide cada línea en campos separados por espacios, numerados `$1`, `$2`... En el formato `combined` quedan así: | Campo | Contenido | Ejemplo | |-------|-----------|---------| | `$1` | IP del cliente | `203.0.113.10` | | `$4` | Fecha y hora (con `[`) | `[25/Sep/2026:10:01:12` | | `$6` | Método (con `"`) | `"GET` | | `$7` | Ruta solicitada | `/css/app.css` | | `$9` | Código de estado | `200` | | `$10` | Bytes enviados | `18234` | El user agent contiene espacios, así que no tiene un número de campo fijo. Para él usarás las comillas como separador, como verás en el paso 3. ## Paso 2: Filtrar líneas con grep `grep` muestra las líneas que coinciden con un patrón. Es el primer filtro en casi cualquier análisis. Cuenta las respuestas 404. Los espacios alrededor evitan que coincida con un `404` dentro de una URL o de un tamaño: ```bash grep -c ' 404 ' access.log ``` ```output 3 ``` Con `-E` usas expresiones regulares extendidas. Esta cuenta todos los errores 4xx y 5xx, anclando el código justo después de las comillas que cierran la petición: ```bash grep -E '" [45][0-9]{2} ' access.log | wc -l ``` ```output 5 ``` `-v` invierte la selección. Es útil para quitar ruido, como los recursos estáticos: ```bash grep -vE '\.(css|js|png|jpg|svg|ico)' access.log | wc -l ``` ```output 9 ``` `-F` busca texto literal, sin interpretar caracteres especiales, y es más rápido. Combinado con un segundo `grep`, responde a "¿cuántas veces ha fallado con 502 la API de pedidos?": ```bash grep -F '/api/orders' access.log | grep -c '" 502 ' ``` ```output 2 ``` `-o` imprime solo la parte que coincide, en lugar de la línea entera. Así extraes las IP y las cuentas: ```bash grep -oE '^[0-9.]+' access.log | sort | uniq -c | sort -rn ``` ```output 3 203.0.113.10 3 198.51.100.23 3 192.0.2.55 1 203.0.113.77 ``` `sort | uniq -c | sort -rn` es el patrón clásico para contar ocurrencias: `uniq -c` cuenta líneas iguales consecutivas (por eso hay que ordenar antes) y `sort -rn` ordena el resultado de mayor a menor. Con `-P` (expresiones compatibles con Perl) puedes usar `\K` para descartar lo que va antes. Esto extrae los usuarios con los que se ha intentado entrar por SSH: ```bash grep -oP 'Failed password for (invalid user )?\K\S+' auth.log | sort | uniq -c | sort -rn ``` ```output 2 root 1 oracle 1 admin ``` Otras opciones que usarás a menudo: - `-i` ignora mayúsculas y minúsculas. - `-A 3`, `-B 3` y `-C 3` muestran 3 líneas después, antes o alrededor de cada coincidencia, útil con trazas de error. - `-n` muestra el número de línea. - `zgrep` busca en logs rotados comprimidos (`zgrep ' 502 ' /var/log/nginx/access.log.*.gz`). ## Paso 3: Trabajar con columnas en awk `awk` procesa cada línea con la forma `condición { acción }`. Si omites la condición, la acción se aplica a todas las líneas. Imprime solo IP, código de estado y ruta: ```bash awk '{print $1, $9, $7}' access.log | head -n 3 ``` ```output 203.0.113.10 200 / 203.0.113.10 200 /css/app.css 198.51.100.23 404 /wp-login.php ``` Una condición filtra por el valor de una columna, algo que con `grep` es mucho menos preciso. Esto muestra todas las peticiones con error de servidor: ```bash awk '$9 >= 500 {print $4, $7, $9}' access.log ``` ```output [25/Sep/2026:10:15:09 /api/orders 502 [25/Sep/2026:11:21:02 /api/orders 502 ``` Distribución de códigos de estado: ```bash awk '{print $9}' access.log | sort | uniq -c | sort -rn ``` ```output 4 200 3 404 2 502 1 201 ``` `awk` también acumula valores. El bloque `END` se ejecuta al terminar de leer el archivo. Total de datos servidos: ```bash awk '{bytes += $10} END {printf "%.1f KiB\n", bytes / 1024}' access.log ``` ```output 38.7 KiB ``` Con arrays asociativos agrupas por cualquier clave. Peticiones y bytes por IP, ordenados por bytes: ```bash awk '{hits[$1]++; bytes[$1] += $10} END {for (ip in hits) printf "%-15s %5d %8d\n", ip, hits[ip], bytes[ip]}' access.log | sort -k3 -rn ``` ```output 203.0.113.10 3 30975 203.0.113.77 1 7390 192.0.2.55 3 744 198.51.100.23 3 486 ``` Peticiones por hora. `split` divide el campo de fecha por `:`, y la segunda parte es la hora: ```bash awk '{split($4, t, ":"); hits[t[2] ":00"]++} END {for (h in hits) print h, hits[h]}' access.log | sort ``` ```output 10:00 7 11:00 3 ``` Con `-F` cambias el separador de campos. Si separas por comillas, el user agent queda en el campo 6: ```bash awk -F'"' '{print $6}' access.log | sort | uniq -c | sort -rn ``` ```output 3 python-requests/2.31 3 curl/8.5.0 3 Mozilla/5.0 (X11; Linux x86_64) 1 Mozilla/5.0 (Macintosh) ``` En `auth.log` la IP no está siempre en la misma columna, porque "invalid user" añade dos palabras. En lugar de un número de campo, recorre la línea y toma la palabra que sigue a `from`: ```bash awk '/Failed password/ {for (i = 1; i <= NF; i++) if ($i == "from") print $(i + 1)}' auth.log | sort | uniq -c | sort -rn ``` ```output 3 198.51.100.23 1 203.0.113.200 ``` Como las marcas de tiempo ISO 8601 se ordenan igual como texto que como fecha, puedes filtrar un intervalo comparando cadenas: ```bash awk '$1 >= "2026-09-25T10:00" && $1 < "2026-09-25T10:10"' auth.log ``` ```output 2026-09-25T10:03:22.004511+00:00 web01 sshd[2188]: Failed password for invalid user oracle from 203.0.113.200 port 51544 ssh2 2026-09-25T10:05:47.310022+00:00 web01 sshd[2203]: Accepted publickey for deploy from 192.0.2.10 port 53120 ssh2: ED25519 SHA256:q1w2e3r4t5y6u7i8o9p0 2026-09-25T10:05:47.412874+00:00 web01 sshd[2203]: pam_unix(sshd:session): session opened for user deploy(uid=1001) by deploy(uid=0) ``` ## Paso 4: Recortar y transformar con sed `sed` edita el texto al vuelo. Sus dos usos principales en logs son imprimir rangos de líneas y sustituir texto. Con `-n` y la orden `p`, `sed` imprime solo las líneas que coinciden. Todas las peticiones de las 11: ```bash sed -n '/25\/Sep\/2026:11:/p' access.log ``` ```output 203.0.113.10 - - [25/Sep/2026:11:20:44 +0000] "GET /products?id=42 HTTP/1.1" 200 7421 "https://example.com/" "Mozilla/5.0 (X11; Linux x86_64)" 192.0.2.55 - - [25/Sep/2026:11:21:02 +0000] "POST /api/orders HTTP/1.1" 502 166 "-" "curl/8.5.0" 203.0.113.77 - - [25/Sep/2026:11:40:19 +0000] "GET /products?id=7 HTTP/1.1" 200 7390 "-" "Mozilla/5.0 (Macintosh)" ``` Un rango `/inicio/,/fin/` imprime desde la primera línea que coincide con el inicio hasta la primera que coincide con el fin. Sirve para extraer la ventana de un incidente: ```bash sed -n '/25\/Sep\/2026:10:15/,/25\/Sep\/2026:11:21/p' access.log ``` ```output 192.0.2.55 - - [25/Sep/2026:10:15:03 +0000] "POST /api/orders HTTP/1.1" 201 412 "-" "curl/8.5.0" 192.0.2.55 - - [25/Sep/2026:10:15:09 +0000] "POST /api/orders HTTP/1.1" 502 166 "-" "curl/8.5.0" 203.0.113.10 - - [25/Sep/2026:11:20:44 +0000] "GET /products?id=42 HTTP/1.1" 200 7421 "https://example.com/" "Mozilla/5.0 (X11; Linux x86_64)" 192.0.2.55 - - [25/Sep/2026:11:21:02 +0000] "POST /api/orders HTTP/1.1" 502 166 "-" "curl/8.5.0" ``` La orden `s/patrón/reemplazo/` sustituye texto, y con `-E` puedes usar grupos. Antes de compartir un log con terceros, anonimiza el último octeto de las IP: ```bash sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+)\.[0-9]+/\1.0/' access.log | head -n 2 ``` ```output 203.0.113.0 - - [25/Sep/2026:10:01:12 +0000] "GET / HTTP/1.1" 200 5320 "-" "Mozilla/5.0 (X11; Linux x86_64)" 203.0.113.0 - - [25/Sep/2026:10:01:13 +0000] "GET /css/app.css HTTP/1.1" 200 18234 "https://example.com/" "Mozilla/5.0 (X11; Linux x86_64)" ``` Quitar la query string antes de contar agrupa las URL que solo cambian en sus parámetros: ```bash sed -E 's/\?[^ ]*//' access.log | awk '{print $7}' | sort | uniq -c | sort -rn ``` ```output 3 /api/orders 2 /products 1 /wp-login.php 1 /css/app.css 1 /admin/config.php 1 /.env 1 / ``` Eliminar la marca de tiempo y el host de `auth.log` deja líneas más cortas y fáciles de leer: ```bash sed -E 's/^\S+ \S+ //' auth.log | head -n 2 ``` ```output sshd[2101]: Failed password for root from 198.51.100.23 port 40122 ssh2 sshd[2101]: Failed password for root from 198.51.100.23 port 40122 ssh2 ``` > Advertencia: `sed -i` modifica el archivo en su sitio. No lo uses sobre logs en uso; trabaja siempre sobre la salida o sobre una copia. ## Paso 5: Combinar las tres herramientas La potencia real aparece al encadenarlas: cada herramienta hace una cosa y pasa el resultado a la siguiente. Rutas que devuelven 404, de más a menos frecuentes. En un log real verás aquí enlaces rotos y escaneos automáticos: ```bash awk '$9 == 404 {print $7}' access.log | sort | uniq -c | sort -rn | head -n 10 ``` ```output 1 /wp-login.php 1 /admin/config.php 1 /.env ``` IP que han provocado 3 o más respuestas 404, candidatas a ser escáneres: ```bash awk '$9 == 404 {print $1}' access.log | sort | uniq -c | awk '$1 >= 3 {print $2}' ``` ```output 198.51.100.23 ``` IP con intentos fallidos de SSH, usando `grep` para extraer y `awk` para quedarse con la dirección: ```bash grep 'Failed password' auth.log | grep -oE 'from [0-9.]+' | awk '{print $2}' | sort | uniq -c | sort -rn ``` ```output 3 198.51.100.23 1 203.0.113.200 ``` ### Seguir un log en tiempo real `tail -F` sigue un archivo aunque se rote. Añade `--line-buffered` a `grep` para que imprima cada coincidencia en cuanto llega en lugar de acumularlas: ```bash sudo tail -F /var/log/nginx/access.log | grep --line-buffered -E '" 5[0-9]{2} ' ``` Pulsa `Ctrl+C` para salir. ### Leer los logs de journald Muchos servicios escriben solo en el journal de systemd. `journalctl` puede volcar su salida como texto para que la proceses igual. En Ubuntu 24.04 el servicio de SSH se llama `ssh`: ```bash sudo journalctl -u ssh --since today --no-pager | grep 'Failed password' | grep -oE 'from [0-9.]+' | sort | uniq -c | sort -rn ``` ### Consejos de rendimiento con logs grandes - Filtra primero con `grep -F` y deja `awk` para el trabajo por columnas sobre menos líneas. - Anteponer `LC_ALL=C` a `grep` y `sort` evita el coste de las reglas de idioma y acelera mucho los archivos de varios GB: `LC_ALL=C grep -F ' 502 ' access.log`. - Para logs rotados y comprimidos usa `zcat` o `zgrep` en lugar de descomprimirlos en disco: `zcat /var/log/nginx/access.log.*.gz | awk '{print $9}' | sort | uniq -c`. ## Conclusión Con `grep` para seleccionar líneas, `awk` para trabajar con columnas y agregar, y `sed` para recortar y transformar, puedes responder en segundos a la mayoría de preguntas sobre lo que ha pasado en un servidor. La clave es construir la tubería paso a paso y comprobar la salida de cada etapa antes de añadir la siguiente. Como siguientes pasos puedes bloquear automáticamente las IP que fallan repetidamente en SSH con Fail2ban, centralizar los logs de varios servidores con rsyslog para analizarlos desde un único punto, o llevarlos a Elasticsearch cuando necesites búsquedas y paneles sobre grandes volúmenes.