Настройка логирования (ELK Stack) для веб-приложения
ELK (Elasticsearch + Logstash + Kibana) или современный вариант с Beats — зрелая платформа для централизованного логирования. Она справляется с объёмами от нескольких гигабайт в сутки до терабайт, но требует внимательной настройки индексов, ресурсов и retention-политики. Без этого кластер быстро деградирует.
Выбор схемы: ELK vs EFK
Классический стек: Filebeat → Logstash → Elasticsearch → Kibana
Упрощённый для небольших объёмов: Filebeat → Elasticsearch (без Logstash, парсинг через ingest pipelines прямо в ES)
Для Kubernetes: Fluent Bit → Elasticsearch → Kibana (Fluent Bit легче Filebeat, нативная интеграция с k8s metadata)
Выбор определяется объёмом трансформаций: если нужны сложные enrichment'ы, grok-парсинг, маршрутизация по нескольким назначениям — Logstash незаменим. Если логи структурированы (JSON) и нужна просто доставка — ingest pipelines достаточно.
Развёртывание через Docker Compose
Для dev/staging окружения:
# docker-compose.yml
version: '3.8'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0
environment:
- discovery.type=single-node
- xpack.security.enabled=true
- xpack.security.http.ssl.enabled=false
- ELASTIC_PASSWORD=changeme
- "ES_JAVA_OPTS=-Xms2g -Xmx2g"
volumes:
- esdata:/usr/share/elasticsearch/data
ports:
- "9200:9200"
ulimits:
memlock:
soft: -1
hard: -1
kibana:
image: docker.elastic.co/kibana/kibana:8.13.0
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
- ELASTICSEARCH_USERNAME=kibana_system
- ELASTICSEARCH_PASSWORD=changeme
ports:
- "5601:5601"
depends_on:
- elasticsearch
logstash:
image: docker.elastic.co/logstash/logstash:8.13.0
volumes:
- ./logstash/pipeline:/usr/share/logstash/pipeline
- ./logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml
ports:
- "5044:5044" # Beats input
- "5000:5000" # TCP input
depends_on:
- elasticsearch
volumes:
esdata:
Logstash Pipeline
Конфигурация для парсинга Nginx access log и application JSON log:
# logstash/pipeline/main.conf
input {
beats {
port => 5044
}
tcp {
port => 5000
codec => json_lines
}
}
filter {
if [fields][log_type] == "nginx_access" {
grok {
match => {
"message" => '%{IPORHOST:client_ip} - %{DATA:user} \[%{HTTPDATE:timestamp}\] "%{WORD:method} %{DATA:request} HTTP/%{NUMBER:http_version}" %{NUMBER:status_code:int} %{NUMBER:bytes_sent:int} "%{DATA:referrer}" "%{DATA:user_agent}" %{NUMBER:request_time:float}'
}
}
date {
match => ["timestamp", "dd/MMM/yyyy:HH:mm:ss Z"]
target => "@timestamp"
}
geoip {
source => "client_ip"
target => "geoip"
}
useragent {
source => "user_agent"
target => "ua"
}
mutate {
remove_field => ["message", "timestamp"]
}
}
if [fields][log_type] == "app_json" {
json {
source => "message"
target => "app"
}
mutate {
remove_field => ["message"]
}
}
}
output {
if [fields][log_type] == "nginx_access" {
elasticsearch {
hosts => ["http://elasticsearch:9200"]
user => "elastic"
password => "changeme"
index => "nginx-access-%{+YYYY.MM.dd}"
}
} else {
elasticsearch {
hosts => ["http://elasticsearch:9200"]
user => "elastic"
password => "changeme"
index => "app-logs-%{+YYYY.MM.dd}"
}
}
}
Filebeat на серверах приложений
# /etc/filebeat/filebeat.yml
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/nginx/access.log
fields:
log_type: nginx_access
fields_under_root: false
multiline:
# Nginx access log — однострочный, multiline не нужен
- type: log
enabled: true
paths:
- /var/www/app/storage/logs/laravel.log
fields:
log_type: app_json
multiline:
pattern: '^\['
negate: true
match: after
max_lines: 50
output.logstash:
hosts: ["logstash-server:5044"]
ssl.enabled: false
# Метаданные хоста
processors:
- add_host_metadata:
when.not.contains.tags: forwarded
- add_fields:
target: ''
fields:
environment: production
service: web-app
Отправка логов приложения напрямую
Для Laravel — через кастомный Monolog handler:
// config/logging.php
'channels' => [
'logstash' => [
'driver' => 'custom',
'via' => App\Logging\LogstashLogger::class,
'host' => env('LOGSTASH_HOST', 'logstash'),
'port' => env('LOGSTASH_PORT', 5000),
'level' => 'debug',
],
'stack' => [
'driver' => 'stack',
'channels' => ['daily', 'logstash'],
],
],
// app/Logging/LogstashLogger.php
namespace App\Logging;
use Monolog\Logger;
use Monolog\Handler\SocketHandler;
use Monolog\Formatter\JsonFormatter;
class LogstashLogger
{
public function __invoke(array $config): Logger
{
$handler = new SocketHandler(
"tcp://{$config['host']}:{$config['port']}"
);
$handler->setFormatter(new JsonFormatter());
return new Logger('app', [$handler]);
}
}
Теперь каждый Log::error(...) отправляет структурированный JSON напрямую в Logstash.
Index Lifecycle Management (ILM)
Без ILM индексы растут бесконтрольно и заполняют диск. Политика для application logs:
PUT _ilm/policy/app-logs-policy
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_size": "5gb",
"max_age": "1d"
},
"set_priority": { "priority": 100 }
}
},
"warm": {
"min_age": "3d",
"actions": {
"shrink": { "number_of_shards": 1 },
"forcemerge": { "max_num_segments": 1 },
"set_priority": { "priority": 50 }
}
},
"cold": {
"min_age": "30d",
"actions": {
"freeze": {},
"set_priority": { "priority": 0 }
}
},
"delete": {
"min_age": "90d",
"actions": { "delete": {} }
}
}
}
}
Index template привязывает политику к паттерну:
PUT _index_template/app-logs-template
{
"index_patterns": ["app-logs-*"],
"template": {
"settings": {
"number_of_shards": 2,
"number_of_replicas": 1,
"index.lifecycle.name": "app-logs-policy",
"index.lifecycle.rollover_alias": "app-logs"
}
}
}
Kibana: основные настройки
После первого запуска:
- Stack Management → Index Patterns → создать паттерн
app-logs-*с полем@timestamp - Discover → выбрать паттерн → убедиться что данные поступают
- Dashboards → создать дашборд с виджетами:
-
Top error messages (Terms aggregation по
app.message.keyword) -
HTTP status distribution (Pie chart по
status_code) -
Request rate (Date histogram по
@timestamp) -
Error rate by service (Bar chart с фильтром
app.level: error)
-
Top error messages (Terms aggregation по
Saved searches для быстрого доступа:
-
app.level: error AND environment: production -
status_code >= 500 -
request_time > 3
Производительность Elasticsearch
Критичные настройки для production:
# /etc/elasticsearch/elasticsearch.yml
cluster.name: app-logging
node.name: es-node-1
# Запрет свопа
bootstrap.memory_lock: true
# Heap — не более 50% RAM, не более 31g (JVM compressed oops limit)
# Устанавливается через ES_JAVA_OPTS или jvm.options
# Slow log для отладки медленных запросов
index.search.slowlog.threshold.query.warn: 2s
index.indexing.slowlog.threshold.index.warn: 1s
# Thread pool для bulk indexing
thread_pool.write.queue_size: 200
Оптимальное количество шардов: 1 шард ≈ 20-40 GB. Слишком много мелких шардов (oversharding) — частая причина деградации кластера.
Алерты через Kibana Alerting
// Watcher для алертинга на 5xx ошибки
PUT _watcher/watch/high-error-rate
{
"trigger": { "schedule": { "interval": "5m" } },
"input": {
"search": {
"request": {
"indices": ["nginx-access-*"],
"body": {
"query": {
"bool": {
"filter": [
{ "range": { "@timestamp": { "gte": "now-5m" } } },
{ "range": { "status_code": { "gte": 500 } } }
]
}
}
}
}
}
},
"condition": {
"compare": { "ctx.payload.hits.total.value": { "gt": 50 } }
},
"actions": {
"notify_telegram": {
"webhook": {
"method": "POST",
"url": "https://api.telegram.org/bot<TOKEN>/sendMessage",
"body": "{\"chat_id\": \"<CHAT_ID>\", \"text\": \"5xx error spike: {{ctx.payload.hits.total.value}} errors in 5m\"}"
}
}
}
}
Сроки
Развёртывание ELK через Docker Compose, настройка Filebeat на 3-5 серверах, базовые pipeline для Nginx и приложения, ILM-политика, начальные дашборды в Kibana: 2-3 рабочих дня.
Полноценная настройка с парсингом всех типов логов, alerting, настройкой безопасности (TLS, RBAC), production-конфигурацией кластера с репликацией: 5-7 дней.
Масштабирование существующего кластера или миграция с другой системы логирования оценивается отдельно после аудита.







