Налаштування логування (ELK Stack) для вашого веб-застосунку
ELK (Elasticsearch + Logstash + Kibana) або сучасний варіант з Beats — це зріла платформа для централізованого логування. Вона справляється з обсягами від кількох гігабайтів на день до терабайтів, але потребує ретельного налаштування індексів, ресурсів та політики збереження. Без цього кластер швидко деградує.
Вибір схеми: ELK vs EFK
Класичний стек: Filebeat → Logstash → Elasticsearch → Kibana
Спрощений для малих обсягів: Filebeat → Elasticsearch (без Logstash, парсинг через ingest pipelines прямо в ES)
Для Kubernetes: Fluent Bit → Elasticsearch → Kibana (Fluent Bit легший ніж Filebeat, нативна інтеграція з k8s metadata)
Вибір залежить від складності трансформацій: якщо потрібні складні збагачення, 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 логів та application JSON логів:
# 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
# Metadata хоста
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 індекси зростають безконтрольно і заповнюють диск. Політика для логів застосунків:
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 по
Збережені пошуки для швидкого доступу:
-
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 серверах, базові pipelines для Nginx та застосунку, ILM-політика, початкові дашборди в Kibana: 2-3 робочі дні.
Повноцінне налаштування з парсингом усіх типів логів, алертингом, налаштуванням безпеки (TLS, RBAC), production-конфігурацією кластера з репліакцією: 5-7 днів.
Масштабування існуючого кластера або міграція з іншої системи логування оцінюється окремо після аудиту.







