Главная/Статьи/Анализ логов Apache и Nginx: практическое руководство

Анализ логов Apache и Nginx: практическое руководство

Полезные команды для анализа access.log и error.log. Поиск ошибок 404, определение географии запросов, выявление подозрительной активности.

ДМ
Дмитрий Мещеряков
📅 14 мая 2024 г.📖 7 мин чтения

Когда сайт «тормозит без причины» или в панели вебмастера внезапно вырастает число 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)

bash
cd /var/log/httpd/
ls -laSh

Основные файлы:

  • access_log — логи доступа
  • error_log — логи ошибок

Nginx

bash
cd /var/log/nginx/
ls -laSh

Основные файлы:

  • access.log — логи доступа
  • error.log — логи ошибок

BitrixVM

bash
# Логи httpd
/var/log/httpd/

# Логи nginx
/var/log/nginx/

# Логи PHP-FPM
/var/log/php-fpm/

Коды ответов HTTP

КодНазваниеОписание
200OKУспешный запрос
206Partial ContentЧастичное содержимое (докачка)
301Moved PermanentlyПостоянный редирект
302FoundВременный редирект
304Not ModifiedРесурс не изменился (кэш)
400Bad RequestНеверный запрос
401UnauthorizedТребуется авторизация
403ForbiddenДоступ запрещён
404Not FoundСтраница не найдена
500Internal Server ErrorОшибка сервера
502Bad GatewayОшибка шлюза
503Service UnavailableСервис недоступен
504Gateway TimeoutТаймаут шлюза

Анализ кодов ответа

Статистика по кодам за сегодня

bash
awk -v d="$(date +%d/%b/%Y)" '$0 ~ d {print $9}' access.log | sort | uniq -c | sort -rn

Пример вывода:

text
45231 200
   1523 304
    892 301
    234 404
     45 500

Топ-20 URL с ошибкой 404

bash
awk '$9 == 404 {print $7}' access.log | sort | uniq -c | sort -rn | head -20

Топ IP-адресов с ошибками 404

bash
awk '$9 == 404 {print $1}' access.log | sort | uniq -c | sort -rn | head -10

Анализ трафика

Топ-20 запрашиваемых URL

bash
awk '{print $7}' access.log | sort | uniq -c | sort -rn | head -20

Топ-20 IP-адресов по количеству запросов

bash
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -20

Запросы с определённого IP

bash
grep "192.168.1.100" access.log | tail -50

Трафик за конкретный час

bash
grep -c "15/Aug/2024:14:" access.log

Поиск подозрительной активности

Запросы к wp-login (WordPress атаки)

bash
grep "wp-login\|wp-admin\|xmlrpc" access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -10

Поиск SQL-инъекций

bash
grep -iE "(union.*select|select.*from|concat\(|information_schema|sleep\()" access.log | head -20

Обратите внимание на шаблон: искать просто слово select бессмысленно — оно встречается в легитимных параметрах вроде ?select=size, и вы утонете в ложных срабатываниях. Признак атаки — не отдельное ключевое слово, а его комбинация с синтаксисом SQL (union ... select, вызов функции, обращение к information_schema).

Поиск попыток сканирования

bash
grep -E "\.(env|git|sql|bak|backup|old)" access.log | awk '{print $1, $7}' | sort | uniq -c | sort -rn

Большие POST-запросы (потенциальные загрузки)

bash
awk '$6 == "\"POST" && $10 > 1000000 {print $1, $7, $10}' access.log

География запросов

Для определения географии нужен пакет geoip-bin:

bash
# Установка (Debian/Ubuntu)
apt install geoip-bin geoip-database

# Установка (CentOS/RHEL)
yum install GeoIP GeoIP-data

Статистика по странам за день

bash
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 и обходится одним проходом.

Оптимизированная версия с кэшированием

bash
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

bash
tail -50 /var/log/php-fpm/error.log

Ошибки 500 с контекстом

bash
grep " 500 " access.log | tail -20

Ошибки Nginx

bash
tail -100 /var/log/nginx/error.log | grep -E "(error|crit|alert|emerg)"

Полезные однострочники

Размер логов

bash
du -sh /var/log/nginx/* /var/log/httpd/* 2>/dev/null

Запросы в реальном времени

bash
tail -f /var/log/nginx/access.log

Запросы в реальном времени с фильтрацией

bash
tail -f /var/log/nginx/access.log | grep --line-buffered "POST\|500\|404"

Среднее время ответа (если настроен формат лога)

bash
awk '{sum+=$NF; count++} END {print sum/count}' access.log

Скрипт комплексного анализа

Создайте файл analyze_logs.sh:

bash
#!/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

Запуск:

bash
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, сортируйте по нему — это гораздо ближе к реальной картине:

bash
awk '{print $NF, $7}' access.log | sort -rn | head -20

404 от ботов и 404 от людей — разные проблемы. Прежде чем чинить битые ссылки, отфильтруйте сканеры по User-Agent, иначе половина списка окажется попытками найти /wp-admin на сайте, где никогда не было WordPress.

Итоги

Порядок разбора, который экономит больше всего времени: сначала распределение кодов ответа (даёт масштаб проблемы), затем топ URL с ошибками (даёт локализацию), и только потом — детальный разбор по IP и User-Agent (даёт причину). Обратный порядок почти всегда заканчивается копанием в частном случае.

💡 Совет

Когда команд становится мало: ставьте GoAccess — он строит интерактивный отчёт из того же access.log и умеет работать в реальном времени. Разовый разбор инцидента удобнее делать в консоли, а вот регулярный мониторинг руками через awk быстро надоедает.

⚠️ Важно

Ротация логов: Не забывайте настраивать logrotate, иначе логи могут занять всё дисковое пространство. Стандартная ротация — еженедельно с хранением 4 архивов.

🚀

Хотите такое же решение?

Настрою окружение под ваш проект, учту специфику инфраструктуры и обучу команду.

Обсудить проект →
Бесплатная консультация · Ответ в течение дня

Комментарии

Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.