1. El encargo, y el método
Una tienda en línea que vende mates y termos tiene su sitio en un servidor Linux. El martes a la mañana llega un mensaje de la dueña:
«Ayer, lunes, me escribieron varios clientes diciendo que no podían terminar la compra. A la tarde andaba todo bien. ¿Me puedes decir qué pasó, y a cuántos les pasó? Y de paso, cuéntame cómo anduvo el sitio el fin de semana largo.»
El servidor no tiene panel de estadísticas. Tiene lo que tienen todos: un archivo de registro, en el que el servidor web anotó una línea por cada pedido que recibió. Como vimos en el tema 39, ese es uno de los registros que las aplicaciones llevan por su cuenta, fuera del journal. Lo copiamos a nuestro equipo con scp, y empezamos.
Un análisis de este tipo sigue siempre el mismo orden, y es el que le da su estructura al tema:
- Reconocer. Qué tamaño tiene el archivo, qué forma tienen sus líneas, qué significa cada campo. mirar
- Medir lo general. Qué período cubre, cuántos pedidos, de cuántos clientes, con qué resultados. contar
- Seguir lo que llama la atención. Cada número raro es una pregunta nueva. investigar
- Limpiar. Separar lo que distorsiona, y volver a medir. depurar
- Informar. Dejar las consultas en un script, y las conclusiones en palabras. entregar
El tercer paso es el que no se puede planificar, y el más interesante. Nadie sabe de antemano qué va a encontrar. Las preguntas buenas salen de mirar las respuestas a las preguntas simples.
2. Conseguir el archivo
Para que todos trabajemos sobre los mismos datos y obtengamos los mismos números, el registro lo fabrica un script. No usa el azar: aplica una fórmula, y produce siempre exactamente el mismo archivo. Crea un directorio para el proyecto, guarda allí el script con el nombre generar-log.sh —con nano, copiando y pegando— y ejecútalo.
#!/bin/bash
# generar-log.sh: fabrica un registro de servidor web para practicar.
# Uso: ./generar-log.sh > acceso.log
# Produce siempre el mismo archivo: no usa el azar, sino una fórmula.
awk '
function azar(n) { x = (x * 75 + 74) % 65537; return x % n }
function linea(ip, h, m, s, metodo, ruta, estado, bytes, origen, agente) {
printf "%s - - [%02d/Oct/2026:%02d:%02d:%02d -0300] \"%s %s HTTP/1.1\" %d %d \"%s\" \"%s\"\n",
ip, dia, h, m, s, metodo, ruta, estado, bytes, origen, agente
}
BEGIN {
x = 41
sitio = "https://tienda.example"
nr = split("/ / / / /productos /productos /productos /productos/12 /productos/42 /productos/42 " \
"/productos/7 /carrito /carrito /nosotros /contacto /blog /blog/ /blog/ofertas " \
"/buscar?q=mate /buscar?q=termo /css/estilo.css /css/estilo.css /js/app.js /js/app.js " \
"/img/logo.png /img/logo.png /img/logo-viejo.png /api/pedidos /api/pedidos " \
"/descargas/catalogo.pdf /login /favicon.ico", rutas, " ")
split("6 4 3 3 4 6 14 30 55 80 95 100 85 70 75 90 95 85 70 60 48 36 22 12", porhora, " ")
na = split("Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0!" \
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/129.0 Safari/537.36!" \
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 Mobile Safari/604.1!" \
"Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 Chrome/129.0 Mobile Safari/537.36", agentes, "!")
nm = split("/wp-login.php /.env /phpmyadmin/ /admin /.git/config /wp-admin/ /backup.zip", malas, " ")
for (dia = 9; dia <= 12; dia++) {
for (h = 0; h < 24; h++) {
n = porhora[h + 1]
if (dia == 10 || dia == 11) n = int(n * 6 / 10)
for (i = 0; i < n; i++) {
seg = int(i * 3600 / n) + azar(int(3600 / n))
m = int(seg / 60); s = seg % 60
k = int(azar(40) * (azar(30) + 10) / 40)
ip = (k % 2 ? "203.0.113." : "198.51.100.") (10 + k)
agente = agentes[k % na + 1]
ruta = rutas[azar(nr) + 1]
metodo = "GET"; estado = 200; origen = "-"
robot = (azar(25) == 0)
if (robot) {
ip = "66.249.66.1"
agente = "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
if (azar(3) == 0) ruta = "/robots.txt"
else if (ruta ~ /pedidos|carrito|login/) ruta = "/productos"
}
caida = (dia == 12 && h == 10 && m >= 5 && m < 40)
if (caida && !robot && azar(3) > 0) ruta = "/api/pedidos"
bytes = 900 + length(ruta) * 317 + azar(400)
if (ruta == "/robots.txt") bytes = 68
if (ruta ~ /^\/(css|js|img)\//) {
origen = sitio "/"
if (azar(8) == 0) { estado = 304; bytes = 0 }
}
if (ruta == "/img/logo-viejo.png") { estado = 404; bytes = 162; origen = sitio "/nosotros" }
if (ruta == "/blog/") { estado = 301; bytes = 178 }
if (ruta == "/descargas/catalogo.pdf") bytes = 2411724
if (ruta == "/api/pedidos") {
metodo = "POST"; estado = 201; bytes = 87; origen = sitio "/carrito"
if (caida) { estado = 500; bytes = 21 }
}
linea(ip, h, m, s, metodo, ruta, estado, bytes, origen, agente)
}
if (dia == 10 && h == 22)
for (q = 0; q < 60; q++)
linea("192.0.2.146", h, 14 + int(q / 4), q * 13 % 60, "GET", malas[azar(nm) + 1], 404, 162, "-", "Mozilla/5.0 zgrab/0.x")
if (dia == 11 && h == 3)
for (q = 0; q < 480; q++)
linea("198.51.100.77", h, 10 + int(q / 12), q % 12 * 5, "POST", "/login", 401, 43, "-", "python-requests/2.31.0")
}
}
}' | LC_ALL=C sort -s -k4,4
No hace falta entenderlo para seguir el tema: es un programa de awk como los del tema 30, con bucles que pertenecen al curso siguiente. Alcanza con reconocer su forma, que es la del tema 41: primera línea, encabezado con el modo de uso, y la salida por el canal estándar, para que quien lo llame decida dónde guardarla.
$ mkdir -p ~/practica/proyecto && cd ~/practica/proyecto
$ nano generar-log.sh # pegar el script, guardar y salir
$ chmod +x generar-log.sh
$ ./generar-log.sh > acceso.log
$ wc -l acceso.log
4202 acceso.log
$ sha256sum acceso.log
de0b475be947ac4f1107067872fba93e646074cc89e46f28f1a5258dc2f0699e acceso.log
Las dos últimas líneas son la verificación. Si wc -l responde 4202 y la suma coincide con la de arriba —el sha256sum que presentamos en el tema 40—, tu archivo es idéntico al de este tema, byte por byte, y todos los resultados que siguen serán los tuyos. Si no coinciden, algo se alteró al copiar el script: bash -n generar-log.sh es el primer control.
3. Reconocer el formato
Antes de preguntar nada, se mira. Las tres consultas de siempre, del tema 12: cuánto pesa, cómo empieza y cómo termina.
$ ls -lh acceso.log | awk '{print $5}'
695K
$ head -n 2 acceso.log
203.0.113.47 - - [09/Oct/2026:00:02:29 -0300] "GET /css/estilo.css HTTP/1.1" 200 5715 "https://tienda.example/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/129.0 Safari/537.36"
203.0.113.37 - - [09/Oct/2026:00:10:04 -0300] "GET /css/estilo.css HTTP/1.1" 200 6036 "https://tienda.example/" "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 Chrome/129.0 Mobile Safari/537.36"
Es el formato que usan por defecto los servidores web más difundidos, nginx y Apache, llamado combinado. Cada línea es un pedido, y tiene siempre las mismas partes en el mismo orden:
203.0.113.47 - - [09/Oct/2026:00:02:29 -0300] "GET /css/estilo.css HTTP/1.1" 200 5715 "https://tienda.example/" "Mozilla/5.0 (Windows…"
│ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ └─ el programa del cliente
│ │ │ │ │ │ │ │ └─ la página desde la que llegó
│ │ │ │ │ │ │ └─ los bytes enviados
│ │ │ │ │ │ └─ el código de estado
│ │ │ │ │ └─ la ruta pedida
│ │ │ │ └─ el método
│ │ │ └─ la fecha, la hora y el huso horario
│ └─┴─ dos campos que casi siempre están vacíos
└─ la dirección IP del cliente
La pregunta práctica es cómo llegar a cada parte. Con el separador por defecto de awk, los espacios, la línea se divide así:
| Campo | Contiene | En la primera línea |
|---|---|---|
$1 | La dirección IP del cliente. | 203.0.113.47 |
$4 | La fecha y la hora, con un corchete delante. | [09/Oct/2026:00:02:29 |
$6 | El método, con una comilla delante: GET pide, POST envía datos. | "GET |
$7 | La ruta pedida. | /css/estilo.css |
$9 | El código de estado, de los del tema 40. | 200 |
$10 | Los bytes de la respuesta. | 5715 |
$ head -n 1 acceso.log | awk '{print $1, $4, $6, $7, $9, $10}'
203.0.113.47 [09/Oct/2026:00:02:29 "GET /css/estilo.css 200 5715
$ awk '{print NF}' acceso.log | sort -n | uniq -c
480 12
60 13
153 15
1003 18
782 19
868 20
856 24
La segunda consulta es una precaución que conviene tomar con todo archivo desconocido: contar cuántos campos tiene cada línea. Aquí hay siete cantidades distintas, de 12 a 24. No es un defecto del archivo: el último dato, el programa del cliente, es un texto con espacios, y cada espacio suma un campo. Los diez primeros campos son estables; del undécimo en adelante, no.
Para los datos que van entre comillas hay otra manera de cortar: usar la comilla como separador. Vista así, la línea tiene siempre siete partes, y las pares son lo que estaba entre comillas.
$ head -n 1 acceso.log | awk -F'"' '{print $2; print $4; print $6}'
GET /css/estilo.css HTTP/1.1
https://tienda.example/
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/129.0 Safari/537.36
Con -F'"' | Contiene |
|---|---|
$2 | El pedido completo: método, ruta y protocolo. |
$4 | El origen: la página en la que estaba el enlace. Un guion, si no se informó. |
$6 | El agente: cómo se identifica el programa del cliente. |
Con esas dos maneras de dividir la línea se llega a todos los datos. Es la primera decisión de cualquier análisis, y la que más errores evita: saber con qué separador se obtiene cada cosa.
4. Período y volumen
¿Qué días cubre el archivo? La primera y la última línea lo dicen, y sed las extrae de una vez:
$ sed -n '1p;$p' acceso.log | awk '{print $4, $5}'
[09/Oct/2026:00:02:29 -0300]
[12/Oct/2026:23:59:30 -0300]
$ awk '{print substr($4, 2, 11)}' acceso.log | uniq -c
1148 09/Oct/2026
743 10/Oct/2026
1163 11/Oct/2026
1148 12/Oct/2026
Cuatro días completos, del viernes 9 al lunes 12 de octubre. En la segunda consulta, substr($4, 2, 11) toma once caracteres del cuarto campo a partir del segundo, salteando el corchete: la fecha sola. Y uniq -c va sin sort delante. En el tema 26 aprendimos que uniq solo junta líneas iguales contiguas, y que por eso se ordena antes; aquí el archivo ya viene ordenado por fecha, y no ordenar conserva el orden cronológico, que alfabéticamente se perdería.
Ya hay algo para anotar. El sábado tuvo menos movimiento que los días hábiles, como era de esperar. El domingo tuvo más que el viernes. Puede ser cierto, o puede ser otra cosa. Lo dejamos pendiente.
5. Los códigos de estado
El noveno campo dice cómo terminó cada pedido. Es la consulta que da el panorama de salud del servidor, y es el modismo del tema 24, sin agregados:
$ awk '{print $9}' acceso.log | sort | uniq -c | sort -rn
3105 200
480 401
209 201
186 404
102 301
91 304
29 500
| Código | Significa | Pedidos | ¿Normal? |
|---|---|---|---|
200 | Correcto. | 3105 | Sí. |
201 | Correcto, y se creó algo: aquí, un pedido de compra. | 209 | Sí. Son las ventas. |
301 | Redirección a otra dirección. | 102 | Sí. |
304 | «No cambió»: el navegador ya tenía una copia. | 91 | Sí. |
401 | Identificación rechazada. | 480 | Son muchísimos. |
404 | La ruta no existe. | 186 | Hay que ver cuáles. |
500 | Error del servidor. | 29 | Nunca es normal. |
Para un informe conviene el porcentaje. Con un arreglo de awk, como los del tema 30, se cuenta y se calcula en una sola pasada; NR, en el bloque END, vale el total de líneas.
$ awk '{n[$9]++} END {for (e in n) printf "%s %5d %5.1f %%\n", e, n[e], n[e] * 100 / NR}' acceso.log | sort
200 3105 73.9 %
201 209 5.0 %
301 102 2.4 %
304 91 2.2 %
401 480 11.4 %
404 186 4.4 %
500 29 0.7 %
Uno de cada nueve pedidos terminó en un 401. Antes de seguir, una advertencia sobre cómo se buscan estos números, porque es el error que arruina más análisis de registros:
$ grep -c '404' acceso.log
201
$ grep -c '" 404 ' acceso.log
186
$ awk '$9 == 404' acceso.log | wc -l
186
La primera consulta cuenta quince líneas que no son errores: tienen la secuencia «404» en otro lado, en la cantidad de bytes o en la hora. grep busca texto, y no sabe de campos. Se lo puede acotar con el contexto, como en la segunda forma, que exige la comilla que cierra el pedido y los espacios alrededor. Pero la manera segura es la tercera: preguntar por el campo. La regla vale para cualquier archivo con columnas: cuando la pregunta es sobre una columna, la herramienta es awk.
Quedan tres números para investigar: los 480, los 186 y los 29. Antes, completamos el panorama.
6. Qué se pide
$ awk '{print $7}' acceso.log | sort -u | wc -l
29
$ awk '{print $7}' acceso.log | sort | uniq -c | sort -rn | head -n 10
586 /login
489 /
322 /productos
238 /api/pedidos
235 /img/logo.png
233 /css/estilo.css
232 /carrito
219 /productos/42
213 /js/app.js
126 /img/logo-viejo.png
Veintinueve rutas distintas, y la más pedida no es la página de inicio sino la de ingreso. Segunda anomalía: en una tienda, la gente mira productos mucho más de lo que inicia sesión.
La lista mezcla dos clases de cosas. Cada vez que alguien abre una página, su navegador pide además la hoja de estilos, el programa y las imágenes; esos pedidos inflan el recuento sin ser visitas. Y los productos aparecen separados por número, cuando interesa el total. Tres retoques, uno por herramienta: grep -v quita los archivos auxiliares por su extensión, y sed borra lo que sigue a un signo de pregunta y unifica los productos.
$ awk '{print $7}' acceso.log |
grep -vE '\.(css|js|png|ico|pdf|txt)$' |
sed -E 's/\?.*//; s#^/productos/[0-9]+#/productos/N#' |
sort | uniq -c | sort -rn | head -n 8
586 /login
489 /
438 /productos/N
322 /productos
238 /api/pedidos
232 /carrito
223 /buscar
118 /nosotros
s/\?.*//
│ │
│ └─ y todo lo que sigue
└─ un signo de pregunta, escapado: en -E es un cuantificador
/buscar?q=mate → /buscar
s#^/productos/[0-9]+#/productos/N#
│ │
│ └─ uno o más dígitos
└─ # como delimitador, para no escapar las barras de la ruta
/productos/42 → /productos/N
Son los recursos del tema 29: dos órdenes separadas por punto y coma, y otro delimitador cuando el texto lleva barras. Ahora se ve lo que importa para el negocio: las fichas de producto suman 438 visitas, hay 223 búsquedas, y 238 pedidos de compra. La tubería quedó de cuatro renglones, partida como enseñó el tema 41.
7. Quién pide
$ awk '{print $1}' acceso.log | sort -u | wc -l
42
$ awk '{print $1}' acceso.log | sort | uniq -c | sort -rn | head -n 6
480 198.51.100.77
229 198.51.100.10
188 203.0.113.11
174 198.51.100.14
171 203.0.113.19
167 198.51.100.16
Cuarenta y dos direcciones distintas. Cinco de las seis primeras tienen cantidades parecidas, entre 160 y 230 pedidos en cuatro días: clientes frecuentes. La primera hizo más del doble que la segunda. Y su cantidad es 480, el mismo número de la sección 5.
Una coincidencia así no se deja pasar. Tercera anomalía, y probablemente la misma que la segunda.
8. Cuándo se pide
La hora está dentro del cuarto campo, después de la fecha. Hay un atajo para llegar a ella: los dos puntos. Usándolos como separador, el segundo campo de cada línea es la hora.
$ awk -F: '{n[$2]++} END {for (h in n) printf "%s %4d %s\n", h, n[h], substr("##################################################", 1, n[h] / 12)}' acceso.log | sort
00 18 #
01 12 #
02 8
03 488 ########################################
04 12 #
05 18 #
06 44 ###
07 96 ########
08 176 ##############
09 256 #####################
10 304 #########################
11 320 ##########################
12 272 ######################
13 224 ##################
14 240 ####################
15 288 ########################
16 304 #########################
17 272 ######################
18 224 ##################
19 192 ################
20 152 ############
21 114 #########
22 130 ##########
23 38 ###
El gráfico se dibuja con una función del tema 30: substr toma, de una tira de cincuenta numerales, tantos como pedidos haya, a razón de uno cada doce. Es un recurso modesto, y alcanza para ver una forma.
La forma es la de cualquier comercio: casi nada de madrugada, un pico a media mañana, una baja al mediodía, otro pico a la tarde. Y una barra que no pertenece a esa curva. A las tres de la madrugada hay más actividad que a las once. ¿De qué día?
$ awk -F: '$2 == "03"' acceso.log | awk '{print substr($4, 2, 11)}' | uniq -c
3 09/Oct/2026
1 10/Oct/2026
481 11/Oct/2026
3 12/Oct/2026
El domingo 11, a las tres de la mañana. Las cuatro anomalías —los 401, la ruta /login, la dirección con 480 pedidos, el domingo abultado— apuntan al mismo lugar. Es hora de seguir el hilo.
9. Hallazgo 1: los ingresos rechazados
Cada pregunta es un filtro sobre el campo del estado, seguido de un recuento. ¿De quién son los 401?
$ awk '$9 == 401 {print $1}' acceso.log | sort | uniq -c
480 198.51.100.77
$ awk '$1 == "198.51.100.77" {print $6, $7, $9}' acceso.log | sort | uniq -c
480 "POST /login 401
$ awk '$9 == 401 {print $4}' acceso.log | sed -n '1p;$p'
[11/Oct/2026:03:10:00
[11/Oct/2026:03:49:55
$ awk '$9 == 401 {print substr($4, 14, 5)}' acceso.log | uniq -c | head -n 3
12 03:10
12 03:11
12 03:12
$ grep -m 1 '^198\.51\.100\.77 ' acceso.log | awk -F'"' '{print $6}'
python-requests/2.31.0
Cinco consultas, cinco datos:
| Pregunta | Respuesta |
|---|---|
| ¿Quién? | Una sola dirección, 198.51.100.77, es responsable de los 480. |
| ¿Qué hizo? | Exclusivamente enviar datos al formulario de ingreso. Nunca pidió otra cosa: ni la página de inicio, ni una imagen. |
| ¿Cuándo? | El domingo 11, de 03:10:00 a 03:49:55. Cuarenta minutos. |
| ¿A qué ritmo? | Doce intentos por minuto, exactos, minuto tras minuto. Uno cada cinco segundos. |
| ¿Con qué? | No con un navegador: con un programa, que se identifica como una biblioteca de Python. |
Ninguna persona escribe una contraseña cada cinco segundos durante cuarenta minutos, a las tres de la mañana, sin mirar una sola página. Es un programa probando contraseñas contra el formulario: lo que se llama un ataque de fuerza bruta. Falta la pregunta que importa: ¿entró?
$ awk '$1 == "198.51.100.77" && $9 != 401' acceso.log | wc -l
0
$ awk '$7 == "/login" {print $6, $9}' acceso.log | sort | uniq -c
106 "GET 200
480 "POST 401
No. De esa dirección no hay ni una respuesta distinta de 401: los 480 intentos fueron rechazados, y después se fue. La segunda consulta da el contexto: en cuatro días, la página de ingreso se mostró 106 veces a visitantes normales, y recibió 480 envíos, todos del atacante.
Quedan explicadas las cuatro anomalías con un solo hecho. Y queda una recomendación para el informe: el servidor aceptó 480 intentos seguidos sin poner ningún obstáculo. Hace falta un límite de intentos por dirección.
10. Hallazgo 2: lo que no existe
Segundo número pendiente: los 186 pedidos de rutas que no existen. ¿Cuáles?
$ awk '$9 == 404 {print $7}' acceso.log | sort | uniq -c | sort -rn
126 /img/logo-viejo.png
13 /phpmyadmin/
10 /.git/config
9 /wp-login.php
9 /.env
8 /backup.zip
7 /wp-admin/
4 /admin
La lista tiene dos mitades que no se parecen en nada: una imagen del propio sitio, y siete rutas que esta tienda nunca tuvo. Un mismo código, dos historias. Las separamos mirando quién las pide.
$ awk '$9 == 404 {print $1}' acceso.log | sort | uniq -c | sort -rn | head -n 4
60 192.0.2.146
12 203.0.113.11
10 203.0.113.13
7 198.51.100.14
$ grep '^192\.0\.2\.146 ' acceso.log | awk '{print $9}' | sort | uniq -c
60 404
$ grep '^192\.0\.2\.146 ' acceso.log | sed -n '1p;$p' | awk '{print $4}'
[10/Oct/2026:22:14:00
[10/Oct/2026:22:28:47
$ grep '^192\.0\.2\.146 ' acceso.log | awk -F'"' '{print $6}' | sort -u
Mozilla/5.0 zgrab/0.x
Una dirección hizo 60 pedidos, todos fallidos, en quince minutos del sábado a la noche, y son exactamente las siete rutas extrañas: 13 más 10 más 9 más 9 más 8 más 7 más 4. Su agente nombra una herramienta de exploración. Es un escáner: un programa que recorre Internet probando, en cada servidor que encuentra, las direcciones donde suelen quedar olvidados archivos de configuración, copias de respaldo y paneles de administración. No atacaba a esta tienda en particular; pasa por todos los servidores del mundo, varias veces por día. Que todo le haya dado 404 es la buena noticia: no había nada de eso para encontrar.
En los patrones de grep, los puntos de la dirección van escapados y hay un ancla al principio. Sin eso, 192.0.2.146 coincidiría también con 192.0.2.1460, o con ese texto en medio de una línea: lo que aprendimos en el tema 28 sobre el punto.
La otra mitad son los 126 pedidos de una imagen. Esos no vienen de un programa de exploración:
$ awk '$7 == "/img/logo-viejo.png" {print $1}' acceso.log | sort -u | wc -l
35
$ awk -F'"' '$2 ~ /logo-viejo/ {print $4}' acceso.log | sort | uniq -c
126 https://tienda.example/nosotros
La pidieron 35 direcciones distintas, casi todos los clientes del sitio, y en los 126 casos el origen —el cuarto campo, cortando por comillas— es la misma página del sitio: /nosotros. Esa página tiene una referencia a una imagen que alguien borró o renombró. Cada visitante que la abre ve un hueco donde debería estar el logotipo. No es un problema de seguridad sino un defecto del sitio, y el registro dice con precisión dónde está: se corrige editando una línea de una página.
El campo del origen es de los menos usados y de los más útiles: ante un enlace roto, responde la pregunta que de otro modo llevaría horas, «¿desde dónde se está llegando a esto?».
11. Hallazgo 3: la mañana del lunes
Tercer número, y el del encargo original: 29 errores del servidor. A diferencia de los anteriores, un 500 no es culpa de quien pide. El pedido era correcto, y el servidor no pudo atenderlo.
$ awk '$9 == 500 {print $6, $7}' acceso.log | sort | uniq -c
29 "POST /api/pedidos
$ awk '$9 == 500 {print substr($4, 2, 14)}' acceso.log | uniq -c
29 12/Oct/2026:10
$ awk '$9 == 500 {print $4}' acceso.log | sed -n '1p;$p'
[12/Oct/2026:10:05:33
[12/Oct/2026:10:38:23
$ awk '$9 == 500 {print substr($4, 14, 4) "0"}' acceso.log | uniq -c
5 10:00
9 10:10
8 10:20
7 10:30
Los 29 son del mismo tipo —el envío de un pedido de compra—, y todos del lunes 12 entre las 10:05 y las 10:38. Ni uno antes, ni uno después, en cuatro días. La última consulta agrupa por tramos de diez minutos, tomando la hora y la decena del minuto: el problema no fue un instante, sino que se mantuvo parejo durante más de media hora.
¿Fallaron todas las compras en ese rato, o algunas?
$ grep '12/Oct/2026:10:' acceso.log | awk '$7 == "/api/pedidos" {print $9}' | sort | uniq -c
4 201
29 500
$ grep '12/Oct/2026:10:' acceso.log | awk '$7 == "/api/pedidos" && $9 == 201 {print substr($4, 14, 8)}'
10:42:57
10:50:58
10:56:58
10:57:29
$ awk '$9 == 500 {print $1}' acceso.log | sort -u | wc -l
18
Todas. En esa hora hubo 33 intentos de compra: 29 fallaron, y los cuatro que funcionaron son posteriores a las 10:42. El servicio de pedidos estuvo completamente caído durante 33 minutos y volvió solo, o alguien lo reinició. El resto del sitio siguió funcionando, y por eso nadie lo notó desde adentro: se podía navegar, buscar y llenar el carrito. Lo único que no se podía hacer era comprar.
Los afectados fueron 18 clientes distintos. Ese es el número que pidió la dueña. El registro del servidor web no dice por qué se cayó; para eso hay que ir, con el horario exacto en la mano, al registro de la aplicación, con el journalctl --since "10:00" --until "10:45" del tema 39. Pero ya se sabe qué buscar, y cuándo.
12. Robots y visitas reales
Para responder la segunda parte del encargo —cómo anduvo el fin de semana— hay que saber qué parte del tráfico son personas. El agente lo dice:
$ awk -F'"' '{print $6}' acceso.log | sort | uniq -c | sort -rn
1003 Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0
868 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/129.0 Safari/537.36
856 Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 Mobile Safari/604.1
782 Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 Chrome/129.0 Mobile Safari/537.36
480 python-requests/2.31.0
153 Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)
60 Mozilla/5.0 zgrab/0.x
Siete agentes. Las cadenas son largas y engañosas —todas dicen «Mozilla», y la de Chrome dice además «Safari»—, por razones históricas que no vienen al caso. Para un informe se las reduce a un nombre, con una orden de sed por familia. El orden de las órdenes importa: primero los robots, luego lo más específico, y Chrome al final, porque su nombre aparece también en las otras.
$ awk -F'"' '{print $6}' acceso.log |
sed -E 's/.*(Googlebot|zgrab|python-requests).*/\1/
s/.*Firefox.*/Firefox/
s/.*iPhone.*/Safari (iPhone)/
s/.*Android.*/Chrome (Android)/
s/.*Chrome.*/Chrome/' |
sort | uniq -c | sort -rn
1650 Chrome
1003 Firefox
856 Safari (iPhone)
480 python-requests
153 Googlebot
60 zgrab
La primera orden usa un grupo y su retrorreferencia, \1: reemplaza la línea entera por el nombre del robot que encontró. Hay un detalle que el resultado delata: la cuarta orden convierte las líneas de Android en Chrome (Android), y la quinta, que busca «Chrome», las vuelve a transformar, porque el texto nuevo también lo contiene. Por eso Chrome suma 1650, que son los 868 de escritorio más los 782 de teléfono. Para este informe sirve, pero es la clase de efecto que hay que verificar con una suma, y que muestra por qué en sed las órdenes se piensan en orden.
Tres de los agentes no son personas: el atacante, el escáner y Googlebot, el programa con que el buscador recorre los sitios para indexarlos, que es un visitante bienvenido. Entre los tres suman 693 líneas. Las apartamos:
$ grep -vE 'Googlebot|zgrab|python-requests' acceso.log > visitas.log
$ wc -l acceso.log visitas.log
4202 acceso.log
3509 visitas.log
7711 total
$ awk '{print substr($4, 2, 11)}' visitas.log | uniq -c
1103 09/Oct/2026
657 10/Oct/2026
659 11/Oct/2026
1090 12/Oct/2026
$ awk '{print $1}' visitas.log | sort -u | wc -l
39
Y aquí se resuelve lo que dejamos pendiente en la sección 4. El domingo no tuvo más movimiento que el viernes: tuvo 659 pedidos de personas, casi los mismos que el sábado. Lo que lo inflaba era el ataque. Con los datos limpios, la respuesta a «cómo anduvo el fin de semana» es otra, y es la correcta: sábado y domingo, un 40 % menos que un día hábil; el lunes, igual que el viernes.
Si hubiéramos informado los números de la sección 4, la dueña habría leído que el domingo fue el mejor día de la semana, y quizá decidido algo con eso. En un sitio pequeño es habitual que la mitad de los pedidos no venga de personas. Toda medición de visitas empieza por decidir qué se cuenta. Y conviene conservar los dos archivos: acceso.log para las preguntas de seguridad, donde los robots son justamente lo que interesa, y visitas.log para las de negocio.
Una última consulta, con dos herramientas que no habíamos usado en el proyecto. ¿Hay clientes que solo produjeron errores, sin un solo pedido correcto? Es una resta de conjuntos: los que tuvieron algún error, menos los que tuvieron algún acierto. La hace comm, del tema 31, sobre dos listas que se arman al vuelo con la sustitución de procesos del tema 25:
$ comm -23 <(awk '$9 >= 400 {print $1}' acceso.log | sort -u) <(awk '$9 < 400 {print $1}' acceso.log | sort -u)
192.0.2.146
198.51.100.77
Dos, y ya los conocemos. Es una consulta que vale la pena recordar: sin saber nada de antemano, señala a los que vinieron a hacer otra cosa que comprar.
13. Cuánto se transfirió
El décimo campo permite sumar, y la suma es el consumo de ancho de banda, que en muchos servicios de alojamiento se paga.
$ awk '{t += $10} END {printf "%d bytes = %.1f MiB\n", t, t / 1024 / 1024}' acceso.log
236468917 bytes = 225.5 MiB
$ awk '{b[$7] += $10} END {for (r in b) printf "%8.1f MiB %s\n", b[r] / 1024 / 1024, r}' acceso.log | sort -rn | head -n 3
213.9 MiB /descargas/catalogo.pdf
1.3 MiB /productos
1.1 MiB /productos/42
$ awk '$7 ~ /catalogo/' acceso.log | wc -l
93
Más de doscientos mebibytes en cuatro días, y un solo archivo explica el 95 %. El catálogo en PDF se descargó 93 veces, el 2 % de los pedidos, y pesa 2,3 MiB cada vez. Todo lo demás junto, las cuatro mil líneas restantes, no llega a doce.
Es un resultado típico, y enseña algo sobre qué medir. Contando pedidos, el catálogo ni siquiera figuraba entre las diez rutas de la sección 6. Sumando bytes, es prácticamente todo el tráfico. La misma tabla, ordenada por otra columna, cuenta otra historia. La recomendación es obvia: comprimir ese PDF.
14. Del registro a una tabla, con sed
Hasta aquí cada consulta volvió a extraer los datos del formato original, con sus corchetes, sus comillas y sus dos separadores. Cuando se van a hacer muchas preguntas conviene hacer el trabajo una vez: convertir el registro en una tabla limpia, con los campos que interesan separados por comas. Es un trabajo para sed, y la expresión se construye de a poco, probando sobre una línea, como enseñó el tema 28.
$ head -n 1 acceso.log | sed -E 's/^([^ ]+) .*/\1/'
203.0.113.47
$ head -n 1 acceso.log | sed -E 's/^([^ ]+) - - \[([^:]+):([^ ]+) .*/\1,\2,\3/'
203.0.113.47,09/Oct/2026,00:02:29
$ head -n 1 acceso.log | sed -E 's/^([^ ]+) - - \[([^:]+):([^ ]+) [^]]+\] "([A-Z]+) ([^ ]+) [^"]+" ([0-9]+) ([0-9]+) .*/\1,\2,\3,\4,\5,\6,\7/'
203.0.113.47,09/Oct/2026,00:02:29,GET,/css/estilo.css,200,5715
La idea es describir la línea entera, de punta a punta, encerrando entre paréntesis lo que se quiere conservar, y reemplazarla por los grupos unidos con comas. Pieza por pieza:
| Pieza | Coincide con | Grupo |
|---|---|---|
^([^ ]+) | Desde el principio, todo lo que no sea un espacio: la dirección. | \1 |
- - \[ | Los dos guiones y el corchete que abre, escapado. | — |
([^:]+) | Hasta los primeros dos puntos: la fecha. | \2 |
:([^ ]+) | Después de los dos puntos, hasta el espacio: la hora. | \3 |
[^]]+\] " | El huso horario, el corchete que cierra y la comilla que abre. | — |
([A-Z]+) | Letras mayúsculas: el método. | \4 |
([^ ]+) | La ruta. | \5 |
[^"]+" | El protocolo, hasta la comilla que cierra. | — |
([0-9]+) ([0-9]+) | El estado y los bytes. | \6, \7 |
.* | Todo el resto, que se descarta. | — |
El recurso que se repite es la clase negada, [^ ]+: «todo lo que no sea un espacio». Es más preciso que .*, que por la codicia del tema 28 se llevaría la línea entera. Ahora, sobre todo el archivo:
$ sed -E 's/^([^ ]+) - - \[([^:]+):([^ ]+) [^]]+\] "([A-Z]+) ([^ ]+) [^"]+" ([0-9]+) ([0-9]+) .*/\1,\2,\3,\4,\5,\6,\7/' acceso.log > acceso.csv
$ head -n 3 acceso.csv
203.0.113.47,09/Oct/2026,00:02:29,GET,/css/estilo.css,200,5715
203.0.113.37,09/Oct/2026,00:10:04,GET,/css/estilo.css,200,6036
66.249.66.1,09/Oct/2026,00:27:31,GET,/robots.txt,200,68
$ wc -l acceso.csv
4202 acceso.csv
$ grep -vc '^[0-9.]*,[0-9]*/Oct/2026,' acceso.csv
0
Las dos últimas líneas son el control de calidad, que no se omite nunca después de una transformación. Cuando la expresión de sed no coincide con una línea, sed no avisa: la deja pasar sin cambios. Por eso se cuenta que estén las 4202, y se cuenta cuántas no tienen la forma nueva. Cero: se convirtieron todas. El archivo pasó de 695 K a 239 K, y tiene siete columnas con nombre fácil:
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|
| dirección | fecha | hora | método | ruta | estado | bytes |
Sobre la tabla, las consultas se vuelven breves, y sirven hasta los filtros más simples del tema 26. Los errores, por día y por tipo, con cut:
$ cut -d, -f2,6 acceso.csv | sort | uniq -c | grep ',[45]'
27 09/Oct/2026,404
90 10/Oct/2026,404
480 11/Oct/2026,401
28 11/Oct/2026,404
41 12/Oct/2026,404
29 12/Oct/2026,500
$ awk -F, '{n[$2]++; b[$2] += $7} END {for (d in n) printf "%s %5d pedidos %6.1f MiB\n", d, n[d], b[d] / 1048576}' acceso.csv | sort
09/Oct/2026 1148 pedidos 65.8 MiB
10/Oct/2026 743 pedidos 43.6 MiB
11/Oct/2026 1163 pedidos 52.7 MiB
12/Oct/2026 1148 pedidos 63.4 MiB
La primera es el resumen de los tres hallazgos en seis líneas: el escáner del sábado, el ataque del domingo, la caída del lunes. Y con la tabla se puede responder una pregunta que con el formato original habría sido engorrosa, y que le interesa a la dueña más que ninguna: de los 18 clientes a los que les falló la compra, ¿cuántos volvieron a intentarlo y pudieron?
$ awk -F, '$6 == 500 {print $1}' acceso.csv | sort -u > fallaron.txt
$ awk -F, '$2 == "12/Oct/2026" && $3 > "10:38:23" && $5 == "/api/pedidos" && $6 == 201 {print $1}' acceso.csv | sort -u > compraron.txt
$ wc -l fallaron.txt compraron.txt
18 fallaron.txt
19 compraron.txt
37 total
$ comm -12 fallaron.txt compraron.txt | wc -l # en las dos listas
11
$ comm -23 fallaron.txt compraron.txt # solo en la primera
198.51.100.24
198.51.100.30
198.51.100.38
203.0.113.11
203.0.113.27
203.0.113.31
203.0.113.33
Dos listas ordenadas y comm. La primera, los que recibieron un error; la segunda, los que completaron una compra ese mismo lunes, después de la última falla. La comparación de horas, $3 > "10:38:23", funciona como comparación de textos porque el formato HH:MM:SS se ordena igual alfabética que cronológicamente. De los 18 afectados, 11 volvieron y compraron. Siete no, y ahí están sus direcciones. Son siete ventas que quizá se puedan recuperar, si la aplicación sabe a qué clientes corresponden.
Una nota para cuando un registro deba compartirse fuera de la empresa: las direcciones IP son datos personales. Otra orden de sed las recorta, dejando solo la red: sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+)\.[0-9]+/\1.0/' acceso.log.
15. El informe, en un script
El análisis está hecho, y si se queda en el historial se pierde. El lunes que viene habrá otro registro y las mismas preguntas. Es el caso exacto del tema 41: las consultas que resultaron útiles se guardan en un archivo, con el registro como argumento.
#!/bin/bash
# informe-log.sh: resume un registro de servidor web en formato combinado.
# Uso: ./informe-log.sh [archivo] (por defecto, acceso.log)
set -eu
log="${1:-acceso.log}"
robots='Googlebot|zgrab|python-requests'
# Si el archivo no se puede leer, el script termina aquí
head -n 1 "$log" > /dev/null
echo "INFORME DE $log"
echo
echo "== Período"
echo "Desde: $(head -n 1 "$log" | awk '{print substr($4, 2)}')"
echo "Hasta: $(tail -n 1 "$log" | awk '{print substr($4, 2)}')"
echo "Pedidos: $(wc -l < "$log")"
echo "Clientes: $(awk '{print $1}' "$log" | sort -u | wc -l)"
echo
echo "== Pedidos por día"
awk '{print substr($4, 2, 11)}' "$log" | uniq -c
echo
echo "== Códigos de estado"
awk '{n[$9]++} END {for (e in n) printf "%s %6d %5.1f %%\n", e, n[e], n[e] * 100 / NR}' "$log" | sort
echo
echo "== Las 5 rutas más pedidas, sin robots"
grep -vE "$robots" "$log" | awk '{print $7}' | sort | uniq -c | sort -rn | head -n 5
echo
echo "== Los 5 clientes más activos"
awk '{print $1}' "$log" | sort | uniq -c | sort -rn | head -n 5
echo
echo "== Errores del servidor (5xx), por hora"
awk '$9 >= 500 {print substr($4, 2, 14)}' "$log" | uniq -c
echo
echo "== Rutas inexistentes más pedidas (404)"
awk '$9 == 404 {print $7}' "$log" | sort | uniq -c | sort -rn | head -n 5
echo
echo "== Ingresos rechazados (401), por cliente"
awk '$9 == 401 {print $1}' "$log" | sort | uniq -c | sort -rn | head -n 5
echo
echo "== Transferencia"
awk '{t += $10} END {printf "%.1f MiB en %d pedidos\n", t / 1048576, NR}' "$log"
Tiene todas las piezas del tema anterior y ninguna otra: la primera línea, el encabezado, set -eu, un argumento con valor por defecto, los datos en variables arriba, y cada variable entre comillas. Tres detalles nuevos, todos pequeños:
| Línea | Por qué está escrita así |
|---|---|
head -n 1 "$log" > /dev/null | No muestra nada. Está para que, si el archivo no existe o no se puede leer, head falle y set -e detenga el script ahí, con un mensaje claro, en lugar de imprimir un informe lleno de errores. |
wc -l < "$log" | Con la redirección, wc no conoce el nombre del archivo y escribe solo el número. Con el nombre como argumento escribiría 4202 acceso.log. |
grep -vE "$robots" | La lista de robots está en una variable, arriba: cuando aparezca uno nuevo se agrega en un solo lugar. Va entre comillas dobles, para que la variable se sustituya. |
$ chmod +x informe-log.sh
$ ./informe-log.sh
INFORME DE acceso.log
== Período
Desde: 09/Oct/2026:00:02:29
Hasta: 12/Oct/2026:23:59:30
Pedidos: 4202
Clientes: 42
== Pedidos por día
1148 09/Oct/2026
743 10/Oct/2026
1163 11/Oct/2026
1148 12/Oct/2026
== Códigos de estado
200 3105 73.9 %
201 209 5.0 %
301 102 2.4 %
304 91 2.2 %
401 480 11.4 %
404 186 4.4 %
500 29 0.7 %
== Las 5 rutas más pedidas, sin robots
480 /
296 /productos
238 /api/pedidos
232 /carrito
229 /img/logo.png
== Los 5 clientes más activos
480 198.51.100.77
229 198.51.100.10
188 203.0.113.11
174 198.51.100.14
171 203.0.113.19
== Errores del servidor (5xx), por hora
29 12/Oct/2026:10
== Rutas inexistentes más pedidas (404)
126 /img/logo-viejo.png
13 /phpmyadmin/
10 /.git/config
9 /wp-login.php
9 /.env
== Ingresos rechazados (401), por cliente
480 198.51.100.77
== Transferencia
225.5 MiB en 4202 pedidos
Una pantalla. Quien la lee sabiendo lo que nosotros sabemos ve los tres hallazgos de un vistazo: una dirección que encabeza la lista de clientes y es la única con ingresos rechazados; errores del servidor concentrados en una hora; una imagen que falta, y un escáner. Lo que nos llevó diez secciones descubrir, el script lo muestra en un segundo, sobre cualquier registro del mismo formato.
$ ./informe-log.sh /noexiste; echo "código: $?"
head: cannot open '/noexiste' for reading: No such file or directory
código: 1
$ ./informe-log.sh visitas.log | sed -n '3,7p'
== Período
Desde: 09/Oct/2026:00:02:29
Hasta: 12/Oct/2026:23:59:30
Pedidos: 3509
Clientes: 39
$ ./informe-log.sh > informe.txt
$ cp informe-log.sh ~/bin/informe-log
Se comporta como un comando: falla con un código distinto de cero cuando no puede trabajar, acepta otro archivo, su salida se redirige y se filtra. Instalado en ~/bin, queda disponible desde cualquier directorio. Y todo lo del tema 41 sobre dónde ejecutar un script se aplica aquí con provecho:
| Para… | Se escribe |
|---|---|
| Analizar el registro actual de un servidor nginx | sudo informe-log /var/log/nginx/access.log |
| Analizar el de la semana pasada, ya rotado y comprimido | zcat /var/log/nginx/access.log.2.gz > /tmp/semana.log; informe-log /tmp/semana.log |
| Analizar el de un servidor remoto, sin copiar nada | ssh servidor 'bash -s' < ~/bin/informe-log |
| Recibir el informe todos los lunes a las 8 | Una línea de crontab: 0 8 * * 1 /home/ana/bin/informe-log … >> … |
| Ver los errores del servidor en el momento en que ocurren | tail -f /var/log/nginx/access.log | awk '$9 >= 500' |
La última fila no usa el script, pero es la continuación natural: los mismos filtros, aplicados a un archivo que crece, convierten el análisis de ayer en la vigilancia de hoy.
16. Las conclusiones, y lo que se usó
El trabajo no termina en los números. La dueña de la tienda no pidió tablas: pidió saber qué pasó. El último paso es escribirlo en su idioma, y cada frase de esta respuesta está respaldada por una consulta que se puede repetir.
Qué pasó el lunes. Entre las 10:05 y las 10:38 no se pudo completar ninguna compra: el servicio de pedidos falló en los 29 intentos de ese rato. El resto del sitio funcionaba, por eso no se notó. A las 10:42 ya andaba de nuevo. Afectó a 18 clientes; 11 volvieron y compraron ese mismo día, y 7 no. Tengo sus direcciones y el horario exacto para que el programador busque la causa.
El fin de semana. Sábado y domingo hubo unas 650 visitas por día, un 40 % menos que un día hábil, que ronda las 1100. En cuatro días se concretaron 209 compras, de 33 clientes distintos.
Tres cosas que no preguntaste. El domingo a las 3 de la mañana, un programa probó 480 contraseñas en el formulario de ingreso, durante 40 minutos. No entró, pero el sitio no le puso ningún límite: hay que agregar uno. La página «Nosotros» tiene una imagen rota, que vieron casi todos los clientes. Y el catálogo en PDF es el 95 % de todo lo que transmite el servidor: conviene achicarlo.
Todo salió de un archivo de texto y de comandos que ya conocías. Vale la pena mirar la lista completa, porque es, en pequeño, el inventario del curso:
| Se usó | Para | Tema |
|---|---|---|
wc, head, tail, ls -lh | Reconocer el archivo. | 7, 12 |
| | Unir cada herramienta con la siguiente. | 24 |
sort | uniq -c | sort -rn | Responder «¿cuál es el más frecuente?»: estados, rutas, clientes, agentes. | 24, 26 |
uniq -c, sin ordenar | Contar por día y por hora, conservando el orden del tiempo. | 26 |
cut | Elegir columnas de la tabla. | 26 |
grep, -v, -c, -E, -m | Elegir líneas: un cliente, una fecha, todo menos los robots. | 27 |
| Anclas, clases, clases negadas, grupos | Describir con exactitud una dirección, una extensión, una línea entera. | 28 |
sed -E 's/…/…/', sed -n '1p;$p' | Unificar rutas, abreviar agentes, convertir el registro en una tabla. | 29 |
awk: campos, -F, patrones, substr, arreglos, printf | Filtrar por columna, extraer la fecha, contar, sumar y calcular porcentajes. | 30 |
comm | Restar y cruzar dos listas de clientes. | 31 |
>, <, <( ), $( ) | Guardar resultados, y usar la salida de un comando como archivo o como texto. | 23, 25 |
sha256sum | Verificar que el archivo es el esperado. | 40 |
Un script con argumentos y set -eu | Dejar el análisis listo para repetirlo. | 41 |
Ninguna de estas herramientas sabe qué es un servidor web. sort ordena líneas, uniq cuenta repetidas, awk separa campos. El mismo procedimiento sirve para un registro de correo, las ventas de un año en CSV, la salida de journalctl o el historial de la shell. Lo que se aprendió no es a analizar registros: es a interrogar texto.
17. Resumen
- Un análisis sigue un orden: reconocer el archivo, medir lo general, investigar lo que llama la atención, limpiar los datos e informar.
- Antes de preguntar, se mira: tamaño, primeras y últimas líneas, y cuántos campos tiene cada línea.
- La primera decisión es con qué separador se llega a cada dato. En el formato combinado, los espacios dan la dirección, la fecha, la ruta, el estado y los bytes; las comillas, el pedido, el origen y el agente.
sort | uniq -c | sort -rnresponde toda pregunta de frecuencia. Sin el primersort,uniq -ccuenta respetando el orden del archivo, que aquí es el del tiempo.- Para preguntar por una columna se usa
awk, nogrep: buscar un número en toda la línea encuentra coincidencias en otros campos. - Cada número raro es una pregunta. Cuatro anomalías distintas —un código, una ruta, un cliente, una hora— resultaron ser el mismo hecho.
- Un hallazgo se establece preguntando quién, qué, cuándo, a qué ritmo y con qué resultado.
- Un mismo código de error puede tener causas sin relación: los 404 eran un enlace roto del sitio y un escáner externo. Se los separa mirando quién pide, y desde dónde.
- Los robots distorsionan las mediciones. Las preguntas de negocio se responden sobre los datos limpios, y las de seguridad, sobre los completos.
- La misma tabla ordenada por otra columna cuenta otra historia: por cantidad de pedidos y por bytes, el primer puesto es distinto.
- Convertir el registro a una tabla con
sedsimplifica las consultas siguientes. Después de transformar, se verifica que todas las líneas hayan cambiado. commresta y cruza listas ordenadas: quiénes están en una y no en otra.- Las consultas útiles se guardan en un script, que recibe el archivo como argumento y se puede ejecutar en otra máquina o a una hora fija.
- El resultado de un análisis no son los números sino las conclusiones, escritas para quien las pidió.
18. Preguntas y desafíos
Las preguntas repasan las decisiones del proyecto. Intenta responder cada una antes de abrirla.
1. ¿Por qué conviene contar los campos de cada línea, con awk '{print NF}', antes de empezar?
Para saber si todas las líneas tienen la misma estructura. Si la cantidad varía, hay campos con espacios adentro, y no se puede contar con la posición de los que vienen después.
2. ¿Por qué el agente se obtiene con awk -F'"' '{print $6}' y no con el separador por defecto?
Porque es un texto con espacios, que con el separador por defecto queda repartido en una cantidad variable de campos. Cortando por comillas es siempre el sexto.
3. En awk '{print substr($4, 2, 11)}' acceso.log | uniq -c no hay sort. ¿Por qué funciona, y qué se ganó?
Funciona porque el registro ya está ordenado por fecha, y las líneas iguales quedan contiguas. No ordenar conserva el orden cronológico de los días.
4. grep -c '404' acceso.log da 201 y awk '$9 == 404' acceso.log | wc -l da 186. ¿Cuál es correcto, y por qué difieren?
El segundo. grep cuenta toda línea que contenga esa secuencia de caracteres, también en los bytes o en la hora. awk pregunta por el campo del estado.
5. ¿Por qué el patrón para buscar una dirección se escribe '^192\.0\.2\.146 '?
El ancla exige que esté al principio de la línea, los puntos escapados son puntos y no «cualquier carácter», y el espacio final impide que coincida con una dirección más larga.
6. ¿Qué datos permitieron afirmar que los 401 eran un ataque y no usuarios que olvidaron su contraseña?
Venían todos de una sola dirección, que no pidió ninguna otra cosa; a un ritmo fijo de doce por minuto durante cuarenta minutos; de madrugada; y desde un programa, no un navegador.
7. ¿Cómo se distinguió el enlace roto del escáner, si los dos producían el código 404?
Por quién pedía y desde dónde: el escáner era una sola dirección, sin origen, pidiendo rutas ajenas al sitio; la imagen la pedían 35 direcciones, todas desde la misma página del sitio.
8. ¿Por qué el recuento de pedidos por día cambió al quitar los robots, y cuál de los dos se informa?
Porque los 480 pedidos del ataque estaban contados en el domingo. Para describir la actividad de los clientes se informa el recuento sin robots.
9. Después de convertir el registro con sed, ¿por qué se verifica con wc -l y con grep -vc?
Porque sed deja pasar sin cambios las líneas que no coinciden con la expresión, y no avisa. Hay que comprobar que estén todas y que todas tengan la forma nueva.
10. ¿Qué hace comm -23 fallaron.txt compraron.txt, y qué requisito tienen los dos archivos?
Muestra las líneas que están en el primero y no en el segundo: los clientes que tuvieron un error y no compraron después. Los dos archivos deben estar ordenados.
11. En el script, ¿para qué sirve la línea head -n 1 "$log" > /dev/null?
Para comprobar que el archivo se puede leer. Si no, head falla, y por set -e el script termina en ese punto con el mensaje de error, antes de escribir nada.
12. ¿Por qué el catálogo no figuraba entre las diez rutas más pedidas y era primero por bytes?
Porque se pide pocas veces pero es muy grande. Cantidad de pedidos y volumen transferido son medidas distintas, y cada una ordena la tabla de otro modo.
Desafíos
Cada uno se resuelve con una línea, sobre acceso.log o acceso.csv. La respuesta está oculta: compara tu número antes de mirar el comando.
1. ¿Cuántos pedidos usaron el método POST?
718. cut -d, -f4 acceso.csv | sort | uniq -c, o awk '$6 == "\"POST"' acceso.log | wc -l.
2. ¿Qué se buscó más en el sitio, «mate» o «termo»?
«mate», 120 veces contra 103. awk '$7 ~ /^\/buscar/ {print $7}' acceso.log | sed 's/.*q=//' | sort | uniq -c.
3. ¿Cuántos clientes distintos concretaron al menos una compra?
33. awk -F, '$5 == "/api/pedidos" && $6 == 201 {print $1}' acceso.csv | sort -u | wc -l.
4. ¿Cuántos pedidos hizo Googlebot, y cuál fue la ruta que más pidió?
153, y /robots.txt, 57 veces. grep Googlebot acceso.log | awk '{print $7}' | sort | uniq -c | sort -rn | head -n 1.
5. Sin contar los robots, ¿cuál fue la hora de más actividad, sumando los cuatro días?
Las 11, con 307 pedidos. awk -F: '{print $2}' visitas.log | sort | uniq -c | sort -rn | head -n 1.
6. ¿Qué día hubo más errores 404?
El sábado 10, con 90: el día del escáner. awk -F, '$6 == 404 {print $2}' acceso.csv | uniq -c.
7. ¿Qué archivo recibió más respuestas 304?
/js/app.js, con 32. awk '$9 == 304 {print $7}' acceso.log | sort | uniq -c | sort -rn.
8. ¿Qué ruta produce todas las redirecciones 301?
/blog/, con la barra final: 102 veces. awk '$9 == 301 {print $7}' acceso.log | sort | uniq -c.
9. ¿Cuántos megabytes se transfirieron el viernes 9?
65.8 MiB. awk -F, '$2 == "09/Oct/2026" {t += $7} END {printf "%.1f\n", t / 1048576}' acceso.csv.
10. ¿Qué cliente hizo más compras, y cuántas?
198.51.100.12, con 17. awk -F, '$5 == "/api/pedidos" && $6 == 201 {print $1}' acceso.csv | sort | uniq -c | sort -rn | head -n 1.
Para seguir
- Agrega al script una sección con las búsquedas más frecuentes, y otra con el reparto por navegador, usando la orden de
sedde la sección 12. Corrige en ella el doble reemplazo de Chrome, para que los teléfonos se cuenten aparte. - Agrega una sección «Sospechosos», con las direcciones que solo produjeron errores, usando
comm. - Haz que el script acepte un segundo argumento optativo: la cantidad de filas de cada lista, con 5 por defecto.
- Modifica
generar-log.sh: cambia el valor inicial dex, genera otro registro y analízalo con tu script. ¿Qué hallazgos se mantienen, y cuáles cambian de magnitud? - Aplica el mismo método a un registro real de tu sistema. Con
journalctl -b --no-pager -o short-isocomo entrada, averigua qué programas escriben más mensajes, a qué hora, y cuántos son errores. Con/var/log/auth.log, si existe, cuántas veces se usósudoy para qué comandos. - Escribe para un compañero, en media página, las conclusiones de ese análisis: qué encontraste, con qué número lo respaldas y qué recomendarías.
Con este proyecto se usó, en un solo trabajo, casi todo lo que el curso enseñó. Queda una página: mirar el camino completo, poner nombre a lo que ahora sabes hacer, y ver hacia dónde sigue. Es el tema 43.