Когда сайт «тормозит без причины» или в панели вебмастера внезапно вырастает число 404, первое место, куда я иду, — access.log. Не APM, не профилировщик, не отладочные var_dump в коде: сырой лог веб-сервера отвечает на вопросы «кто, что и с каким результатом запрашивал» дешевле и быстрее любого другого инструмента, потому что он уже собран и ничего не нужно доустанавливать.
Ниже — набор команд, которые за годы поддержки проектов осели у меня в заметках. Это не справочник по awk, а рабочий чек-лист: от «что вообще происходит на сервере» до «кто нас сканирует».
Про формат лога. Все команды ниже рассчитаны на стандартный combined: $1 — IP, $7 — URL, $9 — код ответа. Если в проекте свой log_format (а в боевых конфигах он свой почти всегда — добавляют $request_time, $upstream_addr), номера полей поедут. Проверьте один раз: head -1 access.log | awk '{print $9}' — должен вернуться трёхзначный код.
Расположение логов
Apache (CentOS/RHEL)
cd /var/log/httpd/
ls -laShОсновные файлы:
access_log— логи доступаerror_log— логи ошибок
Nginx
cd /var/log/nginx/
ls -laShОсновные файлы:
access.log— логи доступаerror.log— логи ошибок
BitrixVM
# Логи httpd
/var/log/httpd/
# Логи nginx
/var/log/nginx/
# Логи PHP-FPM
/var/log/php-fpm/Коды ответов HTTP
| Код | Название | Описание |
|---|---|---|
| 200 | OK | Успешный запрос |
| 206 | Partial Content | Частичное содержимое (докачка) |
| 301 | Moved Permanently | Постоянный редирект |
| 302 | Found | Временный редирект |
| 304 | Not Modified | Ресурс не изменился (кэш) |
| 400 | Bad Request | Неверный запрос |
| 401 | Unauthorized | Требуется авторизация |
| 403 | Forbidden | Доступ запрещён |
| 404 | Not Found | Страница не найдена |
| 500 | Internal Server Error | Ошибка сервера |
| 502 | Bad Gateway | Ошибка шлюза |
| 503 | Service Unavailable | Сервис недоступен |
| 504 | Gateway Timeout | Таймаут шлюза |
Анализ кодов ответа
Статистика по кодам за сегодня
awk -v d="$(date +%d/%b/%Y)" '$0 ~ d {print $9}' access.log | sort | uniq -c | sort -rnПример вывода:
45231 200
1523 304
892 301
234 404
45 500Топ-20 URL с ошибкой 404
awk '$9 == 404 {print $7}' access.log | sort | uniq -c | sort -rn | head -20Топ IP-адресов с ошибками 404
awk '$9 == 404 {print $1}' access.log | sort | uniq -c | sort -rn | head -10Анализ трафика
Топ-20 запрашиваемых URL
awk '{print $7}' access.log | sort | uniq -c | sort -rn | head -20Топ-20 IP-адресов по количеству запросов
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -20Запросы с определённого IP
grep "192.168.1.100" access.log | tail -50Трафик за конкретный час
grep -c "15/Aug/2024:14:" access.logПоиск подозрительной активности
Запросы к wp-login (WordPress атаки)
grep "wp-login\|wp-admin\|xmlrpc" access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -10Поиск SQL-инъекций
grep -iE "(union.*select|select.*from|concat\(|information_schema|sleep\()" access.log | head -20Обратите внимание на шаблон: искать просто слово select бессмысленно — оно встречается в легитимных параметрах вроде ?select=size, и вы утонете в ложных срабатываниях. Признак атаки — не отдельное ключевое слово, а его комбинация с синтаксисом SQL (union ... select, вызов функции, обращение к information_schema).
Поиск попыток сканирования
grep -E "\.(env|git|sql|bak|backup|old)" access.log | awk '{print $1, $7}' | sort | uniq -c | sort -rnБольшие POST-запросы (потенциальные загрузки)
awk '$6 == "\"POST" && $10 > 1000000 {print $1, $7, $10}' access.logГеография запросов
Для определения географии нужен пакет geoip-bin:
# Установка (Debian/Ubuntu)
apt install geoip-bin geoip-database
# Установка (CentOS/RHEL)
yum install GeoIP GeoIP-dataСтатистика по странам за день
cat access.log | grep "$(date +%d/%b/%Y)" | \
awk '{print $1}' | sort | uniq | \
while read ip; do
geoiplookup "$ip" | cut -d: -f2
done | sort | uniq -c | sort -rn | head -20На объёмах больше нескольких тысяч уникальных IP этот вариант работать перестанет: он поднимает отдельный процесс geoiplookup на каждый адрес. На логе с 50 тысячами уникальных IP это десятки минут. Вариант ниже отдаёт вызовы внутрь awk и обходится одним проходом.
Оптимизированная версия с кэшированием
cat access.log | grep "15/Aug/2024" | \
awk '{ "geoiplookup " $1 " | cut -d \\: -f2 " | getline geo; printf("%-20s\n", geo)}' | \
sort | uniq -c | sort -rnАнализ ошибок
Последние 50 ошибок PHP
tail -50 /var/log/php-fpm/error.logОшибки 500 с контекстом
grep " 500 " access.log | tail -20Ошибки Nginx
tail -100 /var/log/nginx/error.log | grep -E "(error|crit|alert|emerg)"Полезные однострочники
Размер логов
du -sh /var/log/nginx/* /var/log/httpd/* 2>/dev/nullЗапросы в реальном времени
tail -f /var/log/nginx/access.logЗапросы в реальном времени с фильтрацией
tail -f /var/log/nginx/access.log | grep --line-buffered "POST\|500\|404"Среднее время ответа (если настроен формат лога)
awk '{sum+=$NF; count++} END {print sum/count}' access.logСкрипт комплексного анализа
Создайте файл analyze_logs.sh:
#!/bin/bash
LOG_FILE="${1:-/var/log/nginx/access.log}"
DATE=$(date +%d/%b/%Y)
echo "=== Анализ логов за $DATE ==="
echo ""
echo "📊 Статистика кодов ответа:"
cat "$LOG_FILE" | grep "$DATE" | awk '{print $9}' | sort | uniq -c | sort -rn | head -10
echo ""
echo "🔥 Топ-10 URL:"
cat "$LOG_FILE" | grep "$DATE" | awk '{print $7}' | sort | uniq -c | sort -rn | head -10
echo ""
echo "👤 Топ-10 IP:"
cat "$LOG_FILE" | grep "$DATE" | awk '{print $1}' | sort | uniq -c | sort -rn | head -10
echo ""
echo "❌ Последние ошибки 404:"
cat "$LOG_FILE" | grep "$DATE" | grep " 404 " | tail -5 | awk '{print $7}'
echo ""
echo "⚠️ Ошибки 500:"
cat "$LOG_FILE" | grep "$DATE" | grep " 500 " | wc -l
echo ""
echo "📈 Всего запросов за день:"
cat "$LOG_FILE" | grep "$DATE" | wc -lЗапуск:
chmod +x analyze_logs.sh
./analyze_logs.sh /var/log/nginx/access.logЧто стоит помнить
Пара вещей, на которых я обжигался и которые не видны из самих команд.
Время в логе — это время сервера, а не ваше. Если сервер живёт в UTC, а вы смотрите отчёты в московском времени, date +%d/%b/%Y в полночь по МСК выдаст «вчерашний» день по логу. При разборе инцидентов это стабильно приводит к «в логах ничего нет».
Топ по количеству запросов ≠ топ по нагрузке. Один IP с 50 тысячами запросов к статике безобиден, а десять запросов к тяжёлому фильтру каталога кладут PHP-FPM. Если в log_format есть $request_time, сортируйте по нему — это гораздо ближе к реальной картине:
awk '{print $NF, $7}' access.log | sort -rn | head -20404 от ботов и 404 от людей — разные проблемы. Прежде чем чинить битые ссылки, отфильтруйте сканеры по User-Agent, иначе половина списка окажется попытками найти /wp-admin на сайте, где никогда не было WordPress.
Итоги
Порядок разбора, который экономит больше всего времени: сначала распределение кодов ответа (даёт масштаб проблемы), затем топ URL с ошибками (даёт локализацию), и только потом — детальный разбор по IP и User-Agent (даёт причину). Обратный порядок почти всегда заканчивается копанием в частном случае.
Когда команд становится мало: ставьте GoAccess — он строит интерактивный отчёт из того же access.log и умеет работать в реальном времени. Разовый разбор инцидента удобнее делать в консоли, а вот регулярный мониторинг руками через awk быстро надоедает.
Ротация логов: Не забывайте настраивать logrotate, иначе логи могут занять всё дисковое пространство. Стандартная ротация — еженедельно с хранением 4 архивов.
Комментарии
Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.