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

URL: https://dmeshcheryakov.ru/blog/apache-nginx-logs-analysis/
Раздел: DevOps
Теги: Nginx, Apache, Linux, Администрирование
Опубликовано: 2024-05-14
Обновлено: 2026-09-07
Автор: Дмитрий Мещеряков (https://dmeshcheryakov.ru)

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

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

| Код | Название | Описание |
|-----|----------|----------|
| 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 | Таймаут шлюза |

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

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

```bash
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

```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 архивов.