LogstashExample1: различия между версиями

Материал из noname.com.ua
Перейти к навигацииПерейти к поиску
 
(не показано 147 промежуточных версий этого же участника)
Строка 4: Строка 4:
 
[[Категория:Grok]]
 
[[Категория:Grok]]
 
[[Категория:nginx]]
 
[[Категория:nginx]]
  +
[[Категория:Filebeat]]
  +
[[Категория:Elasticsearch]]
  +
=Отдельные заметки=
  +
Некоторые вещи были доработаны после написания основной части по опыту использования и вынесены в отдельные заметки
  +
* Logstash DQF [[LogstashDLQ]]
  +
 
=Пример=
 
=Пример=
Это продолжение статьи https://noname.com.ua/mediawiki/index.php?title=Logstash
+
Это дополнение статьи https://noname.com.ua/mediawiki/index.php?title=Logstash
==nginx==
 
Отправка логов nginx в Elastic (один из вариантов) <BR>
 
Тут рассматривается самый простой случай - когда Filebeat не делает преобразований
 
Nginx умеет писать логи в Json
 
   
  +
=Задача=
Как логгировать хедеры https://stackoverflow.com/questions/24380123/how-to-log-all-headers-in-nginx
 
  +
Задача - отправка логов от тестового приложения в Logstash
  +
* Для тестовых устновок - писать максимально подробные логи приложения
  +
** Nginx - настолько подробно насколько это возможно
  +
** Логи которые пишет бекенд (Ruby)
  +
* Для Staging/Prod - менее подробные логи (что бы не перегрузить Elasticsearch)
   
  +
=Реализация=
  +
  +
* Сервер для сбора - Elasticsearch, Logstash, Kibana, Curator
  +
** Elasticsearch - индексация логов и поиск
  +
** Logstash - прием логов и форматирование перед отравкой в Elasticsearch
  +
** Kibana - интерфейс к Elasticsearch
  +
** Curator -удаление старых индексов
  +
* На окружениях которые генерируют логи - Filebeat (Filebeat проще и легковеснее - нет нужды ставить везде Logstash)
  +
<BR>
  +
При реализации <B>НЕ</B> учитывалось (из экономических соображений):
  +
* Отказоустойчивость
  +
** Standalone ElasticSerach (1 нода)
  +
** Один экзкмпляр Logstash
  +
* Балансировка нагрузки отсутвует
  +
* Нет менеджера очередей для логов для сглаживания пиков нагрузки (можно использовать Kafka/RabbitMQ)
  +
  +
<BR>
  +
Итого есть три типа логов которы нужно собирать (и они в разных форматах)
  +
* nginx access.log
  +
* nginx error.log
  +
* ruby applocation log
  +
  +
=Конфигурация источников логов=
  +
==Конфигурация nginx==
  +
* Из-за размера вынесено в отдельную заметку: [[LogstashExample-nginx-config]]
  +
==Результат==
 
<PRE>
 
<PRE>
  +
tail -1 access.log | jq .
log_format nginxlog_json escape=json.
 
'{ "timestamp": "$time_iso8601", '
 
'"remote_addr": "$remote_addr", '
 
'"body_bytes_sent": $body_bytes_sent, '
 
'"request": "$request", '
 
'"request_method": "$request_method", '
 
'"request_time": $request_time, '
 
'"response_status": $status, '
 
'"upstream_status": $upstream_status,'
 
'"upstream_response_time": $upstream_response_time,'
 
'"upstream_connect_time": $upstream_connect_time,'
 
'"upstream_header_time": $upstream_header_time,'
 
'"upstream_addr": "$upstream_addr",'
 
'"host": "$host",'
 
'"http_x_forwarded_for": "$http_x_forwarded_for",'
 
'"http_referrer": "$http_referer", '
 
'"http_user_agent": "$http_user_agent", '
 
'"http_version": "$server_protocol", '
 
'"nginx_access": true }';
 
 
</PRE>
 
</PRE>
  +
Пример лога (не все поля)
 
В лог попадает:
 
 
<PRE>
 
<PRE>
tail -1 /var/log/nginx/access.log.ssl | jq .
 
 
{
 
{
  +
...
"timestamp": "2021-08-08T08:47:59+00:00",
 
  +
"nginx_http_user_agent": "python-requests/2.23.0",
"remote_addr": "159.224.49.4",
 
  +
"nginx_request_headers": " connection='keep-alive' accept='*/*' accept-encoding='gzip, deflate' host='login-anastasiia-env.arturhaunt.com' user-agent='python-requests/2.23.0' ",
"body_bytes_sent": 361,
 
  +
"nginx_time_iso8601": "2021-08-10T03:09:57+00:00",
"request": "POST /internal/bsearch HTTP/1.1",
 
  +
"nginx_time_local": "10/Aug/2021:03:09:57 +0000",
"request_method": "POST",
 
  +
"nginx_response_body": "<html>\r\n<head><title>301 Moved Permanently</title></head>\r\n<body>\r\n<center><h1>301 Moved Permanently</h1></center>\r\n<hr><center>openresty/1.19.3.2</center>\r\n</body>\r\n</html>\r\n",
"request_time": 0.152,
 
  +
...
"response_status": 200,
 
"upstream_status": 200,
 
"upstream_response_time": 0.028,
 
"upstream_connect_time": 0,
 
"upstream_header_time": 0.028,
 
"upstream_addr": "127.0.0.1:5601",
 
"host": "elk.domain.tld",
 
"http_x_forwarded_for": "",
 
"http_referrer": "https://elk.domain.tld/app/discover",
 
"http_user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Safari/605.1.15",
 
"http_version": "HTTP/1.1",
 
"nginx_access": true
 
 
}
 
}
 
</PRE>
 
</PRE>
  +
==Конфигурация приложения==
  +
Формат логов приложения в рамках задачи не могут быть изменены
   
  +
=Конфигурация доставки логов=
==Filebeat==
 
  +
Для доставки логов всех типов используется Filebeat
  +
==Общая часть конфигурации Filebeat==
  +
Это часть конфигурации которая не относится к какому-то определнному логу а общая для всех.
  +
<BR>
  +
===Модули===
  +
Оставега конфигурация по-умолчанию.
 
<PRE>
 
<PRE>
filebeat.inputs:
 
- type: log
 
enabled: true
 
paths:
 
- /var/log/nginx/elk.domain.tld-access.log.ssl
 
exclude_files: ['\.gz$']
 
 
filebeat.config.modules:
 
filebeat.config.modules:
  +
path: ${path.config}/modules.d/*.yml
 
reload.enabled: false
 
reload.enabled: false
  +
</PRE>
  +
<PRE>
 
setup.template.settings:
 
setup.template.settings:
 
index.number_of_shards: 1
 
index.number_of_shards: 1
 
setup.kibana:
 
setup.kibana:
  +
</PRE>
  +
  +
===Output===
  +
Отправлять логи в Logstash (на beat input с авторизацией по сертификатам)
  +
<BR>Подробнее про настройку: https://noname.com.ua/mediawiki/index.php/Logstash
  +
<PRE>
 
output.logstash:
 
output.logstash:
 
hosts: ["elk.domain.tld:5400"]
 
hosts: ["elk.domain.tld:5400"]
Строка 78: Строка 91:
 
ssl.certificate: "/etc/elk-certs/elk-ssl.crt"
 
ssl.certificate: "/etc/elk-certs/elk-ssl.crt"
 
ssl.key: "/etc/elk-certs/elk-ssl.key"
 
ssl.key: "/etc/elk-certs/elk-ssl.key"
  +
</PRE>
  +
Процессинг по-умолчанию добавляет метаданные хоста-источника
  +
<PRE>
 
processors:
 
processors:
 
- add_host_metadata:
 
- add_host_metadata:
Строка 84: Строка 100:
 
- add_docker_metadata: ~
 
- add_docker_metadata: ~
 
- add_kubernetes_metadata: ~
 
- add_kubernetes_metadata: ~
logging.level: debug
+
logging.level: info
 
logging.selectors: ["*"]
 
logging.selectors: ["*"]
 
</PRE>
 
</PRE>
  +
В примере мониторится всего один файл (исключение очевидно лишнее)
 
  +
==Пересылка nginx access logs==
<BR>
 
  +
Предельно простая конфигурация - читать файлы по маске
Результат пересылается на удаленный хост в Logstash
 
<B>Важно</B> Сертификат должен соответствовать хостнейму - тут нельзя указать IP адрес
 
 
<PRE>
 
<PRE>
  +
filebeat.inputs:
hosts: ["elk.domain.tld:5400"]
 
  +
- type: log
  +
enabled: true
  +
paths:
  +
- /var/www/deploy/backend/current/log/nginx/*access.log*
  +
exclude_files: ['\.gz$']
  +
fields:
  +
nginx_logs: "true"
  +
ruby_application_logs: "false"
  +
type: "nginx_access_log_json"
  +
source_hostname: "test-env.domain.tld"
  +
fields_under_root: true
 
</PRE>
 
</PRE>
   
  +
==Пересылка nginx error logs==
==Logstash==
 
  +
Ничем не отличается от access.log
===Базовый пример без преобразований===
 
  +
<BR> Разбор лога на отдельные поля происходит на стороне Logstash.
Минимальный конфиг - слушать Filebeat на порту 5400 (без сертификата - не будет работать)
 
  +
<PRE>
<BR> Результат - писать в файл без всяких преобразований
 
  +
- type: log
  +
enabled: true
  +
paths:
  +
- /var/www/deploy/backend/current/log/nginx/*error.log*
  +
exclude_files: ['\.gz$']
  +
fields:
  +
nginx_logs: "true"
  +
ruby_application_logs: "false"
  +
type: "nginx_error_log"
  +
source_hostname: "some-domain.tld"
  +
fields_under_root: true
  +
</PRE>
  +
  +
==Пересылка ruby application logs==
  +
Немного более сложная сложная конфигурация для многострочного лога
  +
<PRE>
  +
paths:
  +
- /var/www/deploy/backend/shared/log/staging.log
  +
exclude_files: ['\.gz$']
  +
fields:
  +
nginx_logs: "false"
  +
ruby_application_logs: "true"
  +
type: "ruby_application_logs"
  +
source_hostname: "some-domain.tld"
  +
fields_under_root: true
  +
multiline:
  +
pattern: '[A-Z], \[[0-9]{4}'
  +
negate: true
  +
match: after
  +
</PRE>
  +
Пример лога
  +
<PRE>
  +
D, [2021-08-10T11:17:02.270613 #7] DEBUG -- [ContentSelector::FindExternalService]: /usr/src/app/app/services/content_selector/find_external_service.rb:31:in `perform'
  +
^M/usr/src/app/app/services/service.rb:188:in `call!'
  +
^M/usr/src/app/app/services/service.rb:204:in `call'
  +
^M/usr/src/app/app/services/service.rb:110:in `perform'
  +
^M/usr/src/app/lib/activity_organizers/external.rb:32:in `block in vacant_content_and_topic'
  +
^M/usr/src/app/lib/activity_organizers/external.rb:31:in `each'
  +
^M/usr/src/app/lib/activity_organizers/external.rb:31:in `inject'
  +
^M/usr/src/app/lib/activity_organizers/external.rb:31:in `vacant_content_and_topic'
  +
^M/usr/src/app/lib/activity_organizers/external.rb:18:in `perform'
  +
</PRE>
  +
  +
Разделитель(читается : одна большая буква, запятая, пробел, символ "[", 4 цифры):
  +
<PRE>
  +
pattern: '[A-Z], \[[0-9]{4}'
  +
</PRE>
  +
<B>TODO</B>: Скорее всего список A-Z избыточен так как первая буква означает уровень логгирования, для года наверно можно использовать 202[1-9]
  +
<BR>
  +
  +
* Подробнее про разбор многострочных сообщений - [[FilebeatMultiline]]
  +
* Оригинальная документация - https://www.elastic.co/guide/en/beats/filebeat/current/multiline-examples.html (но не слишком понятная для меня)
  +
* TL;DR: использовать <B>negate: true</B> и <B>match: after</B> практически во всех случаях
   
  +
=Logstash=
  +
==Input==
  +
Получение логов на порту 5400 (Это не syslog протокол - отправляет Filebeat, хотя получене логов через сислог тоже возможно) </B>
 
<PRE>
 
<PRE>
 
input {
 
input {
Строка 111: Строка 193:
 
}
 
}
 
}
 
}
  +
</PRE>
output {
 
  +
file {
 
  +
* В настройке собственно ничего нет кроме путей к сертификатам и номера порта.
flush_interval => 5
 
  +
* Все логи доставляются через один Input
gzip => false
 
  +
==Filter==
path => "/var/log/logstash/logstash_debug.log"
 
  +
===Общая для всех логов часть===
  +
<PRE>
  +
filter {
  +
mutate {
  +
add_field => { "nginx" => "nginx_object" }
  +
add_field => { "ruby" => "ruby_object" }
 
}
 
}
  +
mutate {
  +
rename => { "nginx" => "[application_data][nginx][object]" }
  +
rename => { "ruby" => "[application_data][ruby][object]" }
  +
}
  +
...
  +
пропущена часть специфичная для каждого из типов логов
  +
...
 
}
 
}
 
</PRE>
 
</PRE>
  +
====Построчный разбор====
 
  +
=====Добавление полей=====
Результат записи в файл не удовлетворительный
 
  +
Добавить в лог 2 новых поля
  +
<BR>
  +
Этот шаг нужен для того что бы потом добавлять в эти поля соответствующие логи
 
<PRE>
 
<PRE>
  +
mutate {
root@elk:/var/log/logstash# tail -1 logstash_debug.log | jq .
 
  +
add_field => { "nginx" => "nginx_object" }
  +
add_field => { "ruby" => "ruby_object" }
  +
}
 
</PRE>
 
</PRE>
  +
=====Переименование полей=====
Сообщение не было разобрано на поля
 
  +
Переименовать поля. По какой-то причине создать сразу поля с вложениями не получается
 
<PRE>
 
<PRE>
  +
mutate {
{
 
  +
rename => { "nginx" => "[application_data][nginx][object]" }
"message": "{ \"timestamp\": \"2021-08-08T09:20:47+00:00\", \"remote_addr\": \"61.219.11.151\", \"body_bytes_sent\": 163, \"request\": \"GET / HTTP/1.1\", \"request_method\": \"GET\", \"request_time\": 0.213, \"response_status\": 400, \"upstream_status\": ,\"upstream_response_time\": ,\"upstream_connect_time\": ,\"upstream_header_time\": ,\"upstream_addr\": \"\",\"host\": \"elk.arturhaunt.ninja\",\"http_x_forwarded_for\": \"\",\"http_referrer\": \"\", \"http_user_agent\": \"\", \"http_version\": \"HTTP/1.1\", \"nginx_access\": true }",
 
  +
rename => { "ruby" => "[application_data][ruby][object]" }
"cloud": {
 
"availability_zone": "us-east-1f",
 
"region": "us-east-1",
 
"instance": {
 
"id": "i-075866580c26fd42c"
 
},
 
"service": {
 
"name": "EC2"
 
},
 
"machine": {
 
"type": "t3.large"
 
},
 
"image": {
 
"id": "ami-09e67e426f25ce0d7"
 
},
 
"account": {
 
"id": "543591064633"
 
},
 
"provider": "aws"
 
},
 
"@timestamp": "2021-08-08T09:20:52.684Z",
 
"agent": {
 
"hostname": "elk.domain.tld",
 
"id": "aef24fda-f14c-40cf-a388-fa0af443f5d7",
 
"type": "filebeat",
 
"version": "7.14.0",
 
"ephemeral_id": "c53f02a8-0b37-4dae-a3c7-cbf03eabca6a",
 
"name": "elk.arturhaunt.ninja"
 
},
 
"@version": "1",
 
"log": {
 
"offset": 4126,
 
"file": {
 
"path": "/var/log/nginx/elk.domain.tld-access.log.ssl"
 
 
}
 
}
  +
</PRE>
},
 
  +
В результате получим
"host": {
 
  +
<PRE>
"hostname": "elk.domain.tld",
 
  +
{
"id": "ec2385ad84aeea5eb425460c41a5b866",
 
"os": {
 
"type": "linux",
 
"codename": "focal",
 
"name": "Ubuntu",
 
"version": "20.04.2 LTS (Focal Fossa)",
 
"kernel": "5.4.0-1045-aws",
 
"platform": "ubuntu",
 
"family": "debian"
 
},
 
"ip": [
 
"172.31.91.220",
 
"fe80::14b5:eff:feac:ae79"
 
],
 
"name": "elk.domain.tld",
 
"mac": [
 
"16:b5:0e:ac:ae:79"
 
],
 
"architecture": "x86_64",
 
"containerized": false
 
},
 
 
"input": {
 
"input": {
 
"type": "log"
 
"type": "log"
  +
"application_data": {
},
 
"ecs": {
+
"ruby": {
"version": "1.10.0"
+
"object": "ruby_object"
},
+
},
"tags": [
+
"nginx": {
  +
...
"beats_input_codec_plain_applied"
 
  +
</PRE>
]
 
  +
Глобально это сделано для того что бы логи от бекенда и от nginx отличались на уровне application_data а поля верхнего уровня совпадали (но я до сих пор не уверен имеет ли это смысл или может быть лучше писать их в разные индексы)
  +
  +
===Filter для nginx access logs===
  +
Часть фильтра не относящаяся к nginx access log описана в другом разделе
  +
====Конфигурация====
  +
<PRE>
  +
filter {
  +
...
  +
пропущнена часть общая для всех логов
  +
...
  +
  +
if ([type] == "nginx_access_log_json") {
  +
mutate {
  +
add_field => { "log_level" => "INFO" }
  +
}
  +
  +
json {
  +
source => "message"
  +
target => "[application_data][nginx]"
  +
}
  +
  +
mutate {
  +
remove_field => [ "message" ]
  +
}
  +
  +
json {
  +
source => "[application_data][nginx][nginx_request_body]"
  +
target => "[application_data][nginx][nginx_request_body_json]"
  +
tag_on_failure => "request_body_is_not_json"
  +
}
  +
  +
json {
  +
source => "[application_data][nginx][nginx_response_body]"
  +
target => "[application_data][nginx][nginx_response_body_json]"
  +
tag_on_failure => "response_body_is_not_json"
  +
}
  +
  +
kv {
  +
source => "[application_data][nginx][nginx_request_headers]"
  +
target => "[application_data][nginx][nginx_request_headers]"
  +
}
  +
date {
  +
match => [
  +
"[application_data][nginx][nginx_time_iso8601]",
  +
"MMM dd yyyy HH:mm:ss",
  +
"MMM d yyyy HH:mm:ss",
  +
"ISO8601"
  +
]
  +
target => "@application_timestamp"
  +
}
  +
}
  +
...
  +
</PRE>
  +
  +
====Преобразования специфичные для access.log====
  +
<BR>
  +
Условие надожения фильтров - далее специфичные для access logs фильтры.
  +
<PRE>
  +
if ([type] == "nginx_access_log_json") {
  +
</PRE>
  +
=====Добавление поля с уровнем сообщения=====
  +
Модификация лога - для всех сообщений из access.log добавляем уровень INFO
  +
<BR>
  +
<B>TODO(?)</B> Выставлять другой уровень в зависмости от кода ответа?
  +
<PRE>
  +
mutate {
  +
add_field => { "log_level" => "INFO" }
  +
}
  +
</PRE>
  +
  +
=====Разбор сообщения (т.е. поля "message")=====
  +
Этот фильтр разбирает строку как JSON.
  +
  +
<PRE>
  +
json {
  +
source => "message"
  +
target => "[application_data][nginx]"
  +
}
  +
</PRE>
  +
например если Filebeat пришлет строку лога (экранированный JSON)
  +
<PRE>
  +
'{ \"key\": \"value\"} '
  +
</PRE>
  +
то без модификации в логе будет
  +
<PRE>
  +
{
  +
"message": '{ \"key\": \"value\"} '
 
}
 
}
 
</PRE>
 
</PRE>
  +
т.е. в поле message сообщение попадет "как есть"
===Базовый пример с JSON===
 
  +
<BR>
Для того что бы разобрать на поля JSON
 
  +
С фильтром же результат будет
 
<PRE>
 
<PRE>
  +
{
filter {
 
  +
"application_data": {
json {
 
  +
"nginx": {
source => "message"
 
}
+
"key": "value"
  +
}
  +
},
  +
"message": '{ \"key\": \"value\"} '
  +
}
  +
</PRE>
  +
"application_data" --> "nginx" описаны в target => "[application_data][nginx]"
  +
<BR>
  +
Исходный message никуда не девается и удаляить его если не нужен нужно отдельно
  +
  +
=====Удаление лишнего поля=====
  +
Разобраное на предыдущем шаге сообшение больше не нужно.
  +
<BR>
  +
<B>Важно:</B> тут не рассматривается случай еслли вместо JSON в логе прийдет что-то другое, в таком случае это сообшение будет утрачено. Но так-как формат сообщения контролируется со стороны конфигурации nginx то это не проблема.
  +
<PRE>
  +
mutate {
  +
remove_field => [ "message" ]
  +
}
  +
</PRE>
  +
  +
=====Разбор поля [application_data][nginx][nginx_request_body]=====
  +
<PRE>
  +
json {
  +
source => "[application_data][nginx][nginx_request_body]"
  +
target => "[application_data][nginx][nginx_request_body_json]"
  +
tag_on_failure => "request_body_is_not_json"
  +
}
  +
</PRE>
  +
Тут в случае успеха (т.е. если тело запроса это JSON) - разобранный результат сохранить в поле nginx_request_body_json<BR>
  +
Исходное поле не удаляется (на тот случай если в запросе был Не JSON), при ошибке разбора добавляется тег request_body_is_not_json
  +
  +
=====Разбор поля [application_data][nginx][nginx_response_body_json]=====
  +
Аналогично - но разбор ответа котрый мог быть JSON или нет
  +
<PRE>
  +
json {
  +
source => "[application_data][nginx][nginx_response_body]"
  +
target => "[application_data][nginx][nginx_response_body_json]"
  +
tag_on_failure => "response_body_is_not_json"
  +
}
  +
</PRE>
  +
  +
=====Разбор поля с заголовками=====
  +
Поля с заголовками пишуться LUA-кодом:
  +
<PRE>
  +
rowtext = string.format(" %s='%s' ", k, v)
  +
</PRE>
  +
Это сделано специально для простоты разбора.
  +
До разбора заголовки выглядят так:
  +
<PRE>
  +
"nginx_request_headers": " host='domain.tld' accept-language='en-gb' accept-encoding='gzip, deflate, br' connection='keep-alive' content-type='application/json' user-agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Safari/605.1.15' authorization='Basic ...=' kbn-system-request='true' ...
  +
</PRE>
  +
Фильтр разбирает такие строки используя разделитель"<B>=</B>" по-умолчанию
  +
<PRE>
  +
kv {
  +
source => "[application_data][nginx][nginx_request_headers]"
  +
target => "[application_data][nginx][nginx_request_headers]"
  +
}
  +
</PRE>
  +
Этот фильтр берет строку из поля nginx_request_headers и складывает результат работра туде же:
  +
<PRE>
  +
"nginx_request_headers": {
  +
"content-type": "application/x-www-form-urlencoded",
  +
"content-length": "7",
  +
"host": "...",
  +
"user-agent": "curl/7.68.0",
  +
"accept": "*/*",
  +
"authorization": "Basic ...="
  +
</PRE>
  +
<B>TODO</B>: тут можно подумать как разбирать тело запроса в зависимости от <B>content-type</B>
  +
  +
=====Разбор поля с датой=====
  +
Для того что бы использовать время генерации лога, а не время поступления на логстеш. </BR>
  +
Настройка максимально проста - поле из которого брать и как "пробовать разобрать".
  +
<PRE>
  +
date {
  +
match => [
  +
"[application_data][nginx][nginx_time_iso8601]",
  +
"MMM dd yyyy HH:mm:ss",
  +
"MMM d yyyy HH:mm:ss",
  +
"ISO8601"
  +
]
  +
target => "@application_timestamp"
  +
}
  +
</PRE>
  +
  +
===Filter для nginx error logs===
  +
====Конфигурация====
  +
<PRE>
  +
if ([type] == "nginx_error_log") {
  +
grok {
  +
match => [ "message" , "(?<nginx_time>%{YEAR}/%{MONTHNUM}/%{MONTHDAY} %{TIME}) \[%{LOGLEVEL:log_level}\] %{GREEDYDATA:[application_data][nginx][message]}" ]
  +
}
  +
mutate {
  +
remove_field => [ "message" ]
  +
}
  +
date {
  +
match => [
  +
"[nginx_time]",
  +
"yyyy/dd/mm HH:mm:ss"
  +
]
  +
target => "@application_timestamp"
  +
}
  +
mutate {
  +
remove_field => [ "nginx_time" ]
  +
}
  +
 
}
 
}
 
</PRE>
 
</PRE>
  +
====Построчный разбор====
  +
  +
=====Фильтр что бы применять=====
  +
<PRE>
  +
if ([type] == "nginx_error_log") {
  +
</PRE>
  +
=====Разбор с помощью grok=====
  +
<PRE>
  +
grok {
  +
match => [ "message" , "(?<nginx_time>%{YEAR}/%{MONTHNUM}/%{MONTHDAY} %{TIME}) \[%{LOGLEVEL:log_level}\] %{GREEDYDATA:[application_data][nginx][message]}" ]
  +
}
  +
</PRE>
  +
Входные данные выглядят так:
  +
<PRE>
  +
2021/07/06 08:16:18 [notice] 1#1: start worker process 22
  +
</PRE>
  +
Другими словами эьл
  +
* Дата и время
  +
* Уровень сообщения
  +
* Текст сообщения
  +
Конструкция
  +
<PRE>
  +
(?<nginx_time>%{YEAR}/%{MONTHNUM}/%{MONTHDAY} %{TIME})
  +
</PRE>
  +
означает что в результирующее поле <B>nginx_time></B> записать то что попадет под выражение <B>%{YEAR}/%{MONTHNUM}/%{MONTHDAY} %{TIME}</B>
  +
<BR>
  +
Далее - стандартная конструкция grok вида <B>%{имя готового выражения:имя поля куда записать}</B>
  +
* LOGLEVEL --> log_level
  +
* GREEDYDATA (остаток сообщения) --> [application_data][nginx][message] (вложенное поле)
  +
  +
  +
Дебаг GROK: https://grokdebug.herokuapp.com
  +
  +
=====Удаление оригинальных полей=====
  +
<PRE>
  +
mutate {
  +
remove_field => [ "message" ]
  +
}
  +
</PRE>
  +
=====Разбор даты=====
  +
<PRE>
  +
date {
  +
match => [
  +
"[nginx_time]",
  +
"yyyy/dd/mm HH:mm:ss"
  +
]
  +
target => "@application_timestamp"
  +
}
  +
</PRE>
  +
  +
=====Удаление ненужного поля=====
  +
<PRE>
  +
mutate {
  +
remove_field => [ "nginx_time" ]
  +
}
  +
</PRE>
  +
  +
===Filter для ruby application logs===
  +
====Конфигурация====
  +
<PRE>
  +
if ([ruby_application_logs] == "true") {
  +
mutate {
  +
id => "[Meaningful label for your project, not repeated in any other config files] remove ANSI color codes"
  +
gsub => ["message", "\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]", ""]
  +
}
  +
  +
grok {
  +
match => [ "message" , "%{WORD:[application_data][ruby][log_level_short]}, \[%{TIMESTAMP_ISO8601:[application_data][ruby][timestamp]} \#%{NUMBER}\] %{WORD:log_level} -- %{GREEDYDATA:[application_data][ruby][log_message]}" ]
  +
}
  +
  +
mutate {
  +
remove_field => [ "message" ]
  +
}
  +
  +
date {
  +
match => [
  +
"[application_data][ruby][timestamp]",
  +
"MMM dd yyyy HH:mm:ss",
  +
"MMM d yyyy HH:mm:ss",
  +
"ISO8601"
  +
]
  +
target => "@application_timestamp"
  +
}
  +
  +
}
  +
  +
mutate {
  +
uppercase => [ "log_level" ]
  +
}
  +
  +
}
  +
</PRE>
  +
====Построчный разбор====
  +
====Фильтрация====
  +
<PRE>
  +
if ([ruby_application_logs] == "true") {
  +
</PRE>
  +
=====Удаление цветных кодов=====
  +
<PRE>
  +
mutate {
  +
id => "[Meaningful label for your project, not repeated in any other config files] remove ANSI color codes"
  +
gsub => ["message", "\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]", ""]
  +
}
  +
</PRE>
  +
=====Разбор с помощью grok=====
  +
<PRE>
  +
grok {
  +
match => [ "message" , "%{WORD:[application_data][ruby][log_level_short]}, \[%{TIMESTAMP_ISO8601:[application_data][ruby][timestamp]} \#%{NUMBER}\] %{WORD:log_level} -- %{GREEDYDATA:[application_data][ruby][log_message]}" ]
  +
}
  +
</PRE>
  +
Поле message разбирается на поля
  +
* [application_data][ruby][log_level_short] - короткое однобуквенное обозначение уровня лога (D - Debug, E - Error и т.д.)
  +
* [application_data][ruby][timestamp] - время
  +
* log_level - уровень лога, словом
  +
* [application_data][ruby][log_message] - само сообщение до колнца строки
  +
  +
* Дебаг GROK: https://grokdebug.herokuapp.com (что б не забыть)
  +
  +
=====Удаление ненужного поля=====
  +
<PRE>
  +
mutate {
  +
remove_field => [ "message" ]
  +
}
  +
</PRE>
  +
  +
=====Получение даты=====
  +
<PRE>
  +
date {
  +
match => [
  +
"[application_data][ruby][timestamp]",
  +
"MMM dd yyyy HH:mm:ss",
  +
"MMM d yyyy HH:mm:ss",
  +
"ISO8601"
  +
]
  +
target => "@application_timestamp"
  +
}
  +
</PRE>
  +
  +
=====Преобразование к заглавным буквам=====
  +
нужно так как приложения могут писать уровень по-разному - 'Error', 'error', 'ERROR'
  +
<PRE>
  +
mutate {
  +
uppercase => [ "log_level" ]
  +
}
  +
</PRE>
  +
  +
<B>TODO</B> Замена 'Err' -> Error и т.п. если найдется
  +
  +
==Output==
  +
===Общая для всех логов часть===
  +
Общий получатель логов - Elasticsearch
  +
<PRE>
  +
output {
  +
elasticsearch {
  +
hosts => ["localhost:9200"]
  +
index => "application-logs-%{[host][name]}-%{+YYYY.MM.dd}"
  +
document_type => "nginx_logs"
  +
}
  +
...
  +
пропущены отладочные получатели
  +
...
  +
}
  +
</PRE>
  +
===Отладка===
  +
Для того что б удобно смотреть на результат работы фильтров и преобразований самый простой способ - писать логи в файл.
  +
====Для отладки для nginx access logs====
  +
<PRE>
  +
# if ([type] == "nginx_access_log_json") {
  +
# file {
  +
# flush_interval => 5
  +
# gzip => false
  +
# path => "/var/log/logstash/nginx-%{[host][name]}-%{+YYYY.MM.dd}.log"
  +
# }
  +
# }
  +
</PRE>
  +
====Для отладки для nginx error logs====
  +
<PRE>
  +
# if ([type] == "nginx_error_log") {
  +
# file {
  +
# flush_interval => 5
  +
# gzip => false
  +
# path => "/var/log/logstash/nginx-errors-%{[host][name]}-%{+YYYY.MM.dd}.log"
  +
# }
  +
# }
  +
</PRE>
  +
  +
====Для отладки для ruby application logs====
  +
<PRE>
  +
#
  +
# if ([ruby_application_logs] == "true") {
  +
# file {
  +
# flush_interval => 5
  +
# gzip => false
  +
# path => "/var/log/logstash/ruby-%{[host][name]}-%{+YYYY.MM.dd}.log"
  +
# }
  +
# }
  +
</PRE>
  +
  +
=Elasticsearch-Curator=
  +
==Установка==
  +
<PRE>
  +
wget -qO - https://packages.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
  +
echo 'deb [arch=amd64] https://packages.elastic.co/curator/5/debian stable main' > /etc/apt/sources.list.d/curator.list
  +
apt update
  +
</PRE>
  +
<PRE>
  +
apt-cache policy elasticsearch-curator
  +
elasticsearch-curator:
  +
Installed: (none)
  +
Candidate: 5.8.4
  +
Version table:
  +
5.8.4 500
  +
500 https://packages.elastic.co/curator/5/debian stable/main amd64 Packages
  +
</PRE>
  +
  +
<PRE>
  +
apt install elasticsearch-curator
  +
</PRE>
  +
==Конфигурация==
  +
<PRE>
  +
mkdir /etc/elasticsearch-curator
  +
</PRE>
  +
client.yaml
  +
<PRE>
  +
---
  +
client:
  +
hosts:
  +
- 127.0.0.1
  +
port: 9200
  +
url_prefix:
  +
use_ssl: False
  +
certificate:
  +
client_cert:
  +
client_key:
  +
ssl_no_validate: False
  +
username:
  +
password:
  +
timeout: 30
  +
master_only: False
  +
  +
logging:
  +
loglevel: INFO
  +
logfile:
  +
logformat: default
  +
blacklist: ['elasticsearch', 'urllib3']
  +
</PRE>
  +
actions.yaml
  +
<PRE>
  +
actions:
  +
1:
  +
action: delete_indices
  +
description: >-
  +
Delete indices older than 30 days (based on index name), for network-
  +
prefixed indices. Ignore the error if the filter does not result in an
  +
actionable list of indices (ignore_empty_list) and exit cleanly.
  +
options:
  +
ignore_empty_list: True
  +
disable_action: False
  +
filters:
  +
- filtertype: pattern
  +
kind: prefix
  +
value: application-logs-
  +
- filtertype: age
  +
source: name
  +
direction: older
  +
timestring: '%Y.%m.%d'
  +
unit: days
  +
unit_count: 30
  +
</PRE>
  +
==Тестирование==
  +
Для теста ставим unit_count: 1 и проверяем с опцией --dry-run
  +
<PRE>
  +
+ /usr/bin/curator --dry-run --config /etc/elasticsearch-curator/client.yaml /etc/elasticsearch-curator/actions.yaml
  +
2021-08-11 12:42:52,788 INFO Preparing Action ID: 1, "delete_indices"
  +
2021-08-11 12:42:52,788 INFO Creating client object and testing connection
  +
2021-08-11 12:42:52,791 INFO Instantiating client object
  +
2021-08-11 12:42:52,791 INFO Testing client connectivity
  +
/opt/python/3.9.4/lib/python3.9/site-packages/elasticsearch/connection/base.py:200: ElasticsearchWarning: Elasticsearch built-in security features are not enabled. Without authentication, your cluster could be accessible to anyone. See https://www.elastic.co/guide/en/elasticsearch/reference/7.14/security-minimal-setup.html to enable security.
  +
...
  +
/opt/python/3.9.4/lib/python3.9/site-packages/elasticsearch/connection/base.py:200: ElasticsearchWarning: this request accesses system indices: [.apm-agent-configuration, .apm-custom-link, .async-search, .kibana_7.14.0_001, .kibana_task_manager_7.14.0_001, .tasks], but in a future major version, direct access to system indices will be prevented by default
  +
2021-08-11 12:42:52,854 INFO DRY-RUN MODE. No changes will be made.
  +
2021-08-11 12:42:52,854 INFO (CLOSED) indices may be shown that may not be acted on by action "delete_indices".
  +
2021-08-11 12:42:52,854 INFO DRY-RUN: delete_indices: application-logs-env1-2021.08.09 with arguments: {}
  +
2021-08-11 12:42:52,854 INFO DRY-RUN: delete_indices: application-logs-env1-2021.08.10 with arguments: {}
  +
2021-08-11 12:42:52,854 INFO DRY-RUN: delete_indices: application-logs-elk.domain.tld-2021.08.09 with arguments: {}
  +
2021-08-11 12:42:52,855 INFO DRY-RUN: delete_indices: application-logs-elk.doamin.tld-2021.08.10 with arguments: {}
  +
2021-08-11 12:42:52,855 INFO Action ID: 1, "delete_indices" completed.
  +
2021-08-11 12:42:52,855 INFO Job completed.
  +
</PRE>
  +
Индексы для удаления найдены
  +
  +
==Задание Cron==
  +
Надо передлать на systemd-timer но лень
  +
  +
<B>/etc/cron.daily/curator</B>
  +
<PRE>
  +
#!/bin/bash
  +
  +
/usr/bin/curator \
  +
--config \
  +
/etc/elasticsearch-curator/config.yaml \
  +
/etc/elasticsearch-curator/actions.yaml 2>&1 | logger -t "elasticsearch_curator"
  +
</PRE>
  +
  +
=Kibana=
  +
==Установка==
  +
Установка из того же репозитория что и Elasticsearch
  +
<PRE>
  +
sudo apt-get install kibana
  +
systemctl start kibana
  +
systemctl enable kibana
  +
</PRE>
  +
  +
==Конфиги==
  +
===kibana===
  +
<PRE>
  +
server.port: 5601
  +
server.host: "127.0.0.1"
  +
server.publicBaseUrl: "https://elk.domain .tld"
  +
elasticsearch.hosts: ["http://localhost:9200"]
  +
kibana.index: ".kibana"
  +
i18n.locale: "en"
  +
</PRE>
  +
  +
===nginx===
  +
  +
{{#spoiler:show=Полный конфиг Nginx|
  +
<PRE>
  +
user www-data;
  +
worker_processes 1;
  +
error_log /var/log/nginx/error.log debug;
  +
pid /usr/local/openresty/nginx/logs/nginx.pid;
  +
events {
  +
worker_connections 1024;
  +
}
  +
  +
pcre_jit on;
  +
http {
  +
include mime.types;
  +
default_type application/octet-stream;
  +
  +
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
  +
'$status $body_bytes_sent "$http_referer" '
  +
'"$http_user_agent" "$http_x_forwarded_for"';
  +
  +
# Log in JSON Format
  +
log_format nginxlog_json escape=json
  +
'{ '
  +
'"nginx_http_user_agent": "$http_user_agent",'
  +
'"nginx_ancient_browser": "$ancient_browser",'
  +
'"nginx_body_bytes_sent": "$body_bytes_sent",'
  +
'"nginx_bytes_sent": "$bytes_sent",'
  +
'"nginx_connection": "$connection",'
  +
'"nginx_connection_requests": "$connection_requests",'
  +
'"nginx_connections_active": "$connections_active",'
  +
'"nginx_connections_reading": "$connections_reading",'
  +
'"nginx_connections_waiting": "$connections_waiting",'
  +
'"nginx_connections_writing": "$connections_writing",'
  +
'"nginx_content_length": "$content_length",'
  +
'"nginx_content_type": "$content_type",'
  +
'"nginx_cookie_": "$cookie_",'
  +
'"nginx_document_root": "$document_root",'
  +
'"nginx_document_uri": "$document_uri",'
  +
'"nginx_fastcgi_path_info": "$fastcgi_path_info",'
  +
'"nginx_fastcgi_script_name": "$fastcgi_script_name",'
  +
'"nginx_host": "$host",'
  +
'"nginx_hostname": "$hostname",'
  +
'"nginx_https": "$https",'
  +
'"nginx_invalid_referer": "$invalid_referer",'
  +
'"nginx_is_args": "$is_args",'
  +
'"nginx_limit_conn_status": "$limit_conn_status",'
  +
'"nginx_limit_rate": "$limit_rate",'
  +
'"nginx_limit_req_status": "$limit_req_status",'
  +
'"nginx_modern_browser": "$modern_browser",'
  +
'"nginx_msec": "$msec",'
  +
'"nginx_msie": "$msie",'
  +
'"nginx_nginx_version": "$nginx_version",'
  +
'"nginx_proxy_add_x_forwarded_for": "$proxy_add_x_forwarded_for",'
  +
'"nginx_proxy_host": "$proxy_host",'
  +
'"nginx_proxy_port": "$proxy_port",'
  +
'"nginx_proxy_protocol_addr": "$proxy_protocol_addr",'
  +
'"nginx_proxy_protocol_port": "$proxy_protocol_port",'
  +
'"nginx_proxy_protocol_server_addr": "$proxy_protocol_server_addr",'
  +
'"nginx_proxy_protocol_server_port": "$proxy_protocol_server_port",'
  +
'"nginx_query_string": "$query_string",'
  +
'"nginx_realip_remote_addr": "$realip_remote_addr",'
  +
'"nginx_realip_remote_port": "$realip_remote_port",'
  +
'"nginx_remote_addr": "$remote_addr",'
  +
'"nginx_remote_port": "$remote_port",'
  +
'"nginx_remote_user": "$remote_user",'
  +
'"nginx_request": "$request",'
  +
'"nginx_request_headers": "$request_headers",'
  +
'"nginx_request_body": "$request_body",'
  +
'"nginx_request_id": "$request_id",'
  +
'"nginx_request_length": "$request_length",'
  +
'"nginx_request_method": "$request_method",'
  +
'"nginx_request_time": "$request_time",'
  +
'"nginx_request_uri": "$request_uri",'
  +
'"nginx_scheme": "$scheme",'
  +
'"nginx_server_addr": "$server_addr",'
  +
'"nginx_server_name": "$server_name",'
  +
'"nginx_server_port": "$server_port",'
  +
'"nginx_server_port": "$server_port",'
  +
'"nginx_server_protocol": "$server_protocol",'
  +
'"nginx_ssl_cipher": "$ssl_cipher",'
  +
'"nginx_ssl_ciphers": "$ssl_ciphers",'
  +
'"nginx_ssl_client_cert": "$ssl_client_cert",'
  +
'"nginx_ssl_client_escaped_cert": "$ssl_client_escaped_cert",'
  +
'"nginx_ssl_client_fingerprint": "$ssl_client_fingerprint",'
  +
'"nginx_ssl_client_i_dn": "$ssl_client_i_dn",'
  +
'"nginx_ssl_client_raw_cert": "$ssl_client_raw_cert",'
  +
'"nginx_ssl_client_s_dn": "$ssl_client_s_dn",'
  +
'"nginx_ssl_client_serial": "$ssl_client_serial",'
  +
'"nginx_ssl_client_v_end": "$ssl_client_v_end",'
  +
'"nginx_ssl_client_v_remain": "$ssl_client_v_remain",'
  +
'"nginx_ssl_client_v_start": "$ssl_client_v_start",'
  +
'"nginx_ssl_client_verify": "$ssl_client_verify",'
  +
'"nginx_ssl_early_data": "$ssl_early_data",'
  +
'"nginx_ssl_protocol": "$ssl_protocol",'
  +
'"nginx_ssl_server_name": "$ssl_server_name",'
  +
'"nginx_ssl_session_id": "$ssl_session_id",'
  +
'"nginx_ssl_session_reused": "$ssl_session_reused",'
  +
'"nginx_status": "$status",'
  +
'"nginx_tcpinfo_rtt": "$tcpinfo_rtt",'
  +
'"nginx_tcpinfo_rttvar": "$tcpinfo_rttvar",'
  +
'"nginx_tcpinfo_snd_cwnd": "$tcpinfo_snd_cwnd",'
  +
'"nginx_tcpinfo_rcv_space": "$tcpinfo_rcv_space",'
  +
'"nginx_time_iso8601": "$time_iso8601",'
  +
'"nginx_time_local": "$time_local",'
  +
'"nginx_uid_got": "$uid_got",'
  +
'"nginx_uid_reset": "$uid_reset",'
  +
'"nginx_uid_set": "$uid_set",'
  +
'"nginx_upstream_addr": "$upstream_addr",'
  +
'"nginx_upstream_bytes_received": "$upstream_bytes_received",'
  +
'"nginx_upstream_bytes_sent": "$upstream_bytes_sent",'
  +
'"nginx_upstream_bytes_sent": "$upstream_bytes_sent",'
  +
'"nginx_upstream_cache_status": "$upstream_cache_status",'
  +
'"nginx_upstream_connect_time": "$upstream_connect_time",'
  +
'"nginx_upstream_cookie_": "$upstream_cookie_",'
  +
'"nginx_upstream_header_time": "$upstream_header_time",'
  +
'"nginx_upstream_http_": "$upstream_http_",'
  +
'"nginx_upstream_response_length": "$upstream_response_length",'
  +
'"nginx_upstream_response_time": "$upstream_response_time",'
  +
'"nginx_upstream_status": "$upstream_status",'
  +
'"nginx_uri": "$uri",'
  +
'"nginx_response_body": "$response_body"'
  +
'}';
  +
  +
  +
log_format nginxlog_flat
  +
'"timestamp": "$time_iso8601"'
  +
'"remote_addr": "$remote_addr"'
  +
'"body_bytes_sent": $body_bytes_sent'
  +
'"request": "$request"'
  +
'"request_method": "$request_method"'
  +
'"request_time": $request_time'
  +
'"response_status": $status'
  +
'"upstream_status": $upstream_status'
  +
'"upstream_response_time": $upstream_response_time'
  +
'"upstream_connect_time": $upstream_connect_time'
  +
'"upstream_header_time": $upstream_header_time'
  +
'"upstream_addr": "$upstream_addr"'
  +
'"host": "$host",'
  +
'"http_x_forwarded_for": "$http_x_forwarded_for"'
  +
'"http_referrer": "$http_referer" '
  +
'"http_user_agent": "$http_user_agent"'
  +
'"http_version": "$server_protocol"'
  +
'"nginx_access": true';
  +
  +
  +
  +
  +
access_log /var/log/nginx/access.log nginxlog_json;
  +
sendfile on;
  +
#tcp_nopush on;
  +
keepalive_timeout 65;
  +
#gzip on;
  +
#################################################################################
  +
# see https://github.com/auto-ssl/lua-resty-auto-ssl#request_domain for detauls #
  +
#################################################################################
  +
# The "auto_ssl" shared dict should be defined with enough storage space to
  +
# hold your certificate data. 1MB of storage holds certificates for
  +
# approximately 100 separate domains.
  +
lua_shared_dict auto_ssl 1m;
  +
# The "auto_ssl_settings" shared dict is used to temporarily store various settings
  +
# like the secret used by the hook server on port 8999. Do not change or
  +
# omit it.
  +
lua_shared_dict auto_ssl_settings 64k;
  +
# A DNS resolver must be defined for OCSP stapling to function.
  +
#
  +
# This example uses Google's DNS server. You may want to use your system's
  +
# default DNS servers, which can be found in /etc/resolv.conf. If your network
  +
# is not IPv6 compatible, you may wish to disable IPv6 results by using the
  +
# "ipv6=off" flag (like "resolver 8.8.8.8 ipv6=off").
  +
resolver 8.8.8.8 ipv6=off;
  +
# Initial setup tasks.
  +
init_by_lua_block {
  +
auto_ssl = (require "resty.auto-ssl").new()
  +
-- Define a function to determine which SNI domains to automatically handle
  +
-- and register new certificates for. Defaults to not allowing any domains,
  +
-- so this must be configured.
  +
auto_ssl:set("allow_domain", function(domain)
  +
return true
  +
end)
  +
  +
auto_ssl:init()
  +
}
  +
  +
init_worker_by_lua_block {
  +
auto_ssl:init_worker()
  +
}
  +
  +
  +
# Internal server running on port 8999 for handling certificate tasks.
  +
server {
  +
listen 127.0.0.1:8999;
  +
# Increase the body buffer size, to ensure the internal POSTs can always
  +
# parse the full POST contents into memory.
  +
client_body_buffer_size 128k;
  +
client_max_body_size 128k;
  +
access_log /var/log/nginx/ssl-by-lua-access.log;
  +
location / {
  +
content_by_lua_block {
  +
auto_ssl:hook_server()
  +
}
  +
}
  +
}
  +
  +
upstream kibana {
  +
server 127.0.0.1:5601;
  +
}
  +
  +
server {
  +
listen 80 default_server;
  +
root /var/www/backend;
  +
server_name elk.domain.tld;
  +
access_log /var/log/nginx/elk.domain.tld-access.log nginxlog_json;
  +
error_log /var/log/nginx/elk.domain.tld-error.log;
  +
client_max_body_size 500M;
  +
keepalive_timeout 0;
  +
  +
location / {
  +
return 301 https://$host$request_uri;
  +
}
  +
  +
error_page 404 /404.html;
  +
error_page 500 502 503 504 /500.html;
  +
  +
  +
# Endpoint used for performing domain verification with Let's Encrypt.
  +
location /.well-known/acme-challenge/ {
  +
content_by_lua_block {
  +
auto_ssl:challenge_server()
  +
}
  +
}
  +
}
  +
  +
server {
  +
listen 443 ssl;
  +
root /var/www/backend;
  +
server_name elk.domain.tld;
  +
access_log /var/log/nginx/elk.domain.tld-access.log.ssl nginxlog_json;
  +
error_log /var/log/nginx/elk.domain.tld-error.log.ssl;
  +
client_max_body_size 500M;
  +
keepalive_timeout 0;
  +
# Dynamic handler for issuing or returning certs for SNI domains.
  +
ssl_certificate_by_lua_block {
  +
auto_ssl:ssl_certificate()
  +
}
  +
  +
ssl_certificate /etc/ssl/elk.domain.tld-resty-auto-ssl-fallback.crt;
  +
ssl_certificate_key /etc/ssl/elk.domain.tld-resty-auto-ssl-fallback.key;
  +
  +
  +
auth_basic "Restricted Access";
  +
auth_basic_user_file /etc/nginx/htpasswd.users;
  +
  +
lua_need_request_body on;
  +
  +
set $response_body "";
  +
body_filter_by_lua '
  +
local response_body = string.sub(ngx.arg[1], 1, 1000)
  +
ngx.ctx.buffered = (ngx.ctx.buffered or "") .. response_body
  +
if ngx.arg[2] then
  +
ngx.var.response_body = ngx.ctx.buffered
  +
end
  +
';
  +
  +
  +
set_by_lua_block $request_headers{
  +
local h = ngx.req.get_headers()
  +
local request_headers_all = ""
  +
for k, v in pairs(h) do
  +
local rowtext = ""
  +
rowtext = string.format(" %s='%s' ", k, v)
  +
request_headers_all = request_headers_all .. rowtext
  +
  +
end
  +
return request_headers_all
  +
}
  +
  +
  +
location / {
  +
proxy_headers_hash_max_size 512;
  +
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  +
proxy_set_header Host $http_host;
  +
proxy_redirect off;
  +
proxy_pass http://kibana;
  +
  +
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  +
proxy_set_header X-Forwarded-Proto $scheme;
  +
proxy_set_header X-Forwarded-Ssl on; # Optional
  +
proxy_set_header X-Forwarded-Port $server_port;
  +
proxy_set_header X-Forwarded-Host $host;
  +
}
  +
# Endpoint used for performing domain verification with Let's Encrypt.
  +
location /.well-known/acme-challenge/ {
  +
auth_basic off;
  +
allow all; # Allow all to see content
  +
content_by_lua_block {
  +
auto_ssl:challenge_server()
  +
}
  +
}
  +
error_page 404 /404.html;
  +
error_page 500 502 503 504 /500.html;
  +
  +
}
  +
}
  +
  +
</PRE>
  +
}}
  +
  +
=Дополнения=
  +
==Docker Autodiscovery==
  +
Добавлено в конце - для сбора логов докеа файлбит умеет автодискавери [[Filebeat]]
  +
Формат логов немного отличается
  +
<PRE>
  +
if ([docker_autodiscovery] == "true") {
  +
mutate {
  +
id => "[Meaningful label for your project, not repeated in any other config files] remove ANSI color codes for Docker"
  +
gsub => ["message", "\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]", ""]
  +
}
  +
  +
grok {
  +
match => [ "message" , "%{TIMESTAMP_ISO8601:[application_data][ruby][timestamp]} %{NUMBER}%{GREEDYDATA:[application_data][ruby][log_message]}" ]
  +
}
  +
  +
mutate {
  +
remove_field => [ "message" ]
  +
}
  +
  +
date {
  +
match => [
  +
"[application_data][ruby][timestamp]",
  +
"MMM dd yyyy HH:mm:ss",
  +
"MMM d yyyy HH:mm:ss",
  +
"ISO8601"
  +
]
  +
target => "@application_timestamp"
  +
}
  +
}
  +
</PRE>
  +
  +
=Ссылки=
  +
* https://pawelurbanek.com/elk-nginx-logs-setup

Текущая версия на 14:28, 2 сентября 2021

Отдельные заметки

Некоторые вещи были доработаны после написания основной части по опыту использования и вынесены в отдельные заметки

Пример

Это дополнение статьи https://noname.com.ua/mediawiki/index.php?title=Logstash

Задача

Задача - отправка логов от тестового приложения в Logstash

  • Для тестовых устновок - писать максимально подробные логи приложения
    • Nginx - настолько подробно насколько это возможно
    • Логи которые пишет бекенд (Ruby)
  • Для Staging/Prod - менее подробные логи (что бы не перегрузить Elasticsearch)

Реализация

  • Сервер для сбора - Elasticsearch, Logstash, Kibana, Curator
    • Elasticsearch - индексация логов и поиск
    • Logstash - прием логов и форматирование перед отравкой в Elasticsearch
    • Kibana - интерфейс к Elasticsearch
    • Curator -удаление старых индексов
  • На окружениях которые генерируют логи - Filebeat (Filebeat проще и легковеснее - нет нужды ставить везде Logstash)


При реализации НЕ учитывалось (из экономических соображений):

  • Отказоустойчивость
    • Standalone ElasticSerach (1 нода)
    • Один экзкмпляр Logstash
  • Балансировка нагрузки отсутвует
  • Нет менеджера очередей для логов для сглаживания пиков нагрузки (можно использовать Kafka/RabbitMQ)


Итого есть три типа логов которы нужно собирать (и они в разных форматах)

  • nginx access.log
  • nginx error.log
  • ruby applocation log

Конфигурация источников логов

Конфигурация nginx

Результат

tail -1 access.log | jq .

Пример лога (не все поля)

{
  ...
  "nginx_http_user_agent": "python-requests/2.23.0",
  "nginx_request_headers": " connection='keep-alive'  accept='*/*'  accept-encoding='gzip, deflate'  host='login-anastasiia-env.arturhaunt.com'  user-agent='python-requests/2.23.0' ",
  "nginx_time_iso8601": "2021-08-10T03:09:57+00:00",
  "nginx_time_local": "10/Aug/2021:03:09:57 +0000",
  "nginx_response_body": "<html>\r\n<head><title>301 Moved Permanently</title></head>\r\n<body>\r\n<center><h1>301 Moved Permanently</h1></center>\r\n<hr><center>openresty/1.19.3.2</center>\r\n</body>\r\n</html>\r\n",
  ...
}

Конфигурация приложения

Формат логов приложения в рамках задачи не могут быть изменены

Конфигурация доставки логов

Для доставки логов всех типов используется Filebeat

Общая часть конфигурации Filebeat

Это часть конфигурации которая не относится к какому-то определнному логу а общая для всех.

Модули

Оставега конфигурация по-умолчанию.

filebeat.config.modules:
  path: ${path.config}/modules.d/*.yml
  reload.enabled: false
setup.template.settings:
  index.number_of_shards: 1
setup.kibana:

Output

Отправлять логи в Logstash (на beat input с авторизацией по сертификатам)
Подробнее про настройку: https://noname.com.ua/mediawiki/index.php/Logstash

output.logstash:
  hosts: ["elk.domain.tld:5400"]
  ssl.certificate_authorities: ["/etc/elk-certs/elk-ssl.crt"]
  ssl.certificate: "/etc/elk-certs/elk-ssl.crt"
  ssl.key: "/etc/elk-certs/elk-ssl.key"

Процессинг по-умолчанию добавляет метаданные хоста-источника

processors:
  - add_host_metadata:
      when.not.contains.tags: forwarded
  - add_cloud_metadata: ~
  - add_docker_metadata: ~
  - add_kubernetes_metadata: ~
logging.level: info
logging.selectors: ["*"]

Пересылка nginx access logs

Предельно простая конфигурация - читать файлы по маске

filebeat.inputs:
- type: log
  enabled: true
  paths:
    - /var/www/deploy/backend/current/log/nginx/*access.log*
  exclude_files: ['\.gz$']
  fields:
    nginx_logs: "true"
    ruby_application_logs: "false"
    type: "nginx_access_log_json"
    source_hostname: "test-env.domain.tld"
  fields_under_root: true

Пересылка nginx error logs

Ничем не отличается от access.log
Разбор лога на отдельные поля происходит на стороне Logstash.

- type: log
  enabled: true
  paths:
    - /var/www/deploy/backend/current/log/nginx/*error.log*
  exclude_files: ['\.gz$']
  fields:
    nginx_logs: "true"
    ruby_application_logs: "false"
    type: "nginx_error_log"
    source_hostname: "some-domain.tld"
  fields_under_root: true

Пересылка ruby application logs

Немного более сложная сложная конфигурация для многострочного лога

  paths:
    - /var/www/deploy/backend/shared/log/staging.log
  exclude_files: ['\.gz$']
  fields:
    nginx_logs: "false"
    ruby_application_logs: "true"
    type: "ruby_application_logs"
    source_hostname: "some-domain.tld"
  fields_under_root: true
  multiline:
    pattern: '[A-Z], \[[0-9]{4}'
    negate: true
    match: after

Пример лога

D, [2021-08-10T11:17:02.270613 #7] DEBUG -- [ContentSelector::FindExternalService]: /usr/src/app/app/services/content_selector/find_external_service.rb:31:in `perform'
^M/usr/src/app/app/services/service.rb:188:in `call!'
^M/usr/src/app/app/services/service.rb:204:in `call'
^M/usr/src/app/app/services/service.rb:110:in `perform'
^M/usr/src/app/lib/activity_organizers/external.rb:32:in `block in vacant_content_and_topic'
^M/usr/src/app/lib/activity_organizers/external.rb:31:in `each'
^M/usr/src/app/lib/activity_organizers/external.rb:31:in `inject'
^M/usr/src/app/lib/activity_organizers/external.rb:31:in `vacant_content_and_topic'
^M/usr/src/app/lib/activity_organizers/external.rb:18:in `perform'

Разделитель(читается : одна большая буква, запятая, пробел, символ "[", 4 цифры):

pattern: '[A-Z], \[[0-9]{4}'

TODO: Скорее всего список A-Z избыточен так как первая буква означает уровень логгирования, для года наверно можно использовать 202[1-9]

Logstash

Input

Получение логов на порту 5400 (Это не syslog протокол - отправляет Filebeat, хотя получене логов через сислог тоже возможно)

input {
    beats {
        port => 5400
        ssl => true
        ssl_certificate_authorities => ["/etc/elk-certs/elk-ssl.crt"]
        ssl_certificate => "/etc/elk-certs/elk-ssl.crt"
        ssl_key => "/etc/elk-certs/elk-ssl.key"
        ssl_verify_mode => "force_peer"
    }
}
  • В настройке собственно ничего нет кроме путей к сертификатам и номера порта.
  • Все логи доставляются через один Input

Filter

Общая для всех логов часть

filter {
    mutate {
        add_field => { "nginx" => "nginx_object" }
        add_field => { "ruby" => "ruby_object" }
    }
    mutate {
        rename => { "nginx" => "[application_data][nginx][object]" }
        rename => { "ruby"  => "[application_data][ruby][object]" }
    }
   ... 
   пропущена часть специфичная для каждого из типов логов
   ...
}

Построчный разбор

Добавление полей

Добавить в лог 2 новых поля
Этот шаг нужен для того что бы потом добавлять в эти поля соответствующие логи

    mutate {
        add_field => { "nginx" => "nginx_object" }
        add_field => { "ruby" => "ruby_object" }
    }
Переименование полей

Переименовать поля. По какой-то причине создать сразу поля с вложениями не получается

    mutate {
        rename => { "nginx" => "[application_data][nginx][object]" }
        rename => { "ruby"  => "[application_data][ruby][object]" }
    }

В результате получим

{
  "input": {
    "type": "log"
  "application_data": {
    "ruby": {
      "object": "ruby_object"
    },
    "nginx": {
...

Глобально это сделано для того что бы логи от бекенда и от nginx отличались на уровне application_data а поля верхнего уровня совпадали (но я до сих пор не уверен имеет ли это смысл или может быть лучше писать их в разные индексы)

Filter для nginx access logs

Часть фильтра не относящаяся к nginx access log описана в другом разделе

Конфигурация

filter {
    ... 
    пропущнена часть общая для всех логов
    ...

    if ([type] == "nginx_access_log_json") {
        mutate {
            add_field => { "log_level" => "INFO" }
        }

        json {
            source => "message"
            target => "[application_data][nginx]"
        }

        mutate {
            remove_field => [ "message" ]
        }

        json {
            source => "[application_data][nginx][nginx_request_body]"
            target => "[application_data][nginx][nginx_request_body_json]"
            tag_on_failure => "request_body_is_not_json"
        }

        json {
            source => "[application_data][nginx][nginx_response_body]"
            target => "[application_data][nginx][nginx_response_body_json]"
            tag_on_failure => "response_body_is_not_json"
        }

        kv {
            source => "[application_data][nginx][nginx_request_headers]"
            target => "[application_data][nginx][nginx_request_headers]"
        }
        date {
            match => [
                "[application_data][nginx][nginx_time_iso8601]",
                "MMM dd yyyy HH:mm:ss",
                "MMM  d yyyy HH:mm:ss",
                "ISO8601"
            ]
            target => "@application_timestamp"
        }
    }
...

Преобразования специфичные для access.log


Условие надожения фильтров - далее специфичные для access logs фильтры.

    if ([type] == "nginx_access_log_json") {
Добавление поля с уровнем сообщения

Модификация лога - для всех сообщений из access.log добавляем уровень INFO
TODO(?) Выставлять другой уровень в зависмости от кода ответа?

        mutate {
            add_field => { "log_level" => "INFO" }
        }
Разбор сообщения (т.е. поля "message")

Этот фильтр разбирает строку как JSON.

        json {
            source => "message"
            target => "[application_data][nginx]"
        }

например если Filebeat пришлет строку лога (экранированный JSON)

'{ \"key\": \"value\"} '

то без модификации в логе будет

{
"message": '{ \"key\": \"value\"} '
}

т.е. в поле message сообщение попадет "как есть"
С фильтром же результат будет

{
"application_data": {
  "nginx": {
     "key": "value"
  }
},
"message": '{ \"key\": \"value\"} '
}

"application_data" --> "nginx" описаны в target => "[application_data][nginx]"
Исходный message никуда не девается и удаляить его если не нужен нужно отдельно

Удаление лишнего поля

Разобраное на предыдущем шаге сообшение больше не нужно.
Важно: тут не рассматривается случай еслли вместо JSON в логе прийдет что-то другое, в таком случае это сообшение будет утрачено. Но так-как формат сообщения контролируется со стороны конфигурации nginx то это не проблема.

        mutate {
            remove_field => [ "message" ]
        }
Разбор поля [application_data][nginx][nginx_request_body]
        json {
            source => "[application_data][nginx][nginx_request_body]"
            target => "[application_data][nginx][nginx_request_body_json]"
            tag_on_failure => "request_body_is_not_json"
        }

Тут в случае успеха (т.е. если тело запроса это JSON) - разобранный результат сохранить в поле nginx_request_body_json
Исходное поле не удаляется (на тот случай если в запросе был Не JSON), при ошибке разбора добавляется тег request_body_is_not_json

Разбор поля [application_data][nginx][nginx_response_body_json]

Аналогично - но разбор ответа котрый мог быть JSON или нет

        json {
            source => "[application_data][nginx][nginx_response_body]"
            target => "[application_data][nginx][nginx_response_body_json]"
            tag_on_failure => "response_body_is_not_json"
        }
Разбор поля с заголовками

Поля с заголовками пишуться LUA-кодом:

 rowtext = string.format(" %s='%s' ", k, v)

Это сделано специально для простоты разбора. До разбора заголовки выглядят так:

"nginx_request_headers": " host='domain.tld'  accept-language='en-gb'  accept-encoding='gzip, deflate, br'  connection='keep-alive'  content-type='application/json'  user-agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Safari/605.1.15'  authorization='Basic ...='  kbn-system-request='true' ...

Фильтр разбирает такие строки используя разделитель"=" по-умолчанию

        kv {
            source => "[application_data][nginx][nginx_request_headers]"
            target => "[application_data][nginx][nginx_request_headers]"
        }

Этот фильтр берет строку из поля nginx_request_headers и складывает результат работра туде же:

      "nginx_request_headers": {
        "content-type": "application/x-www-form-urlencoded",
        "content-length": "7",
        "host": "...",
        "user-agent": "curl/7.68.0",
        "accept": "*/*",
        "authorization": "Basic ...="

TODO: тут можно подумать как разбирать тело запроса в зависимости от content-type

Разбор поля с датой

Для того что бы использовать время генерации лога, а не время поступления на логстеш.
Настройка максимально проста - поле из которого брать и как "пробовать разобрать".

        date {
            match => [
                "[application_data][nginx][nginx_time_iso8601]",
                "MMM dd yyyy HH:mm:ss",
                "MMM  d yyyy HH:mm:ss",
                "ISO8601"
            ]
            target => "@application_timestamp"
        }

Filter для nginx error logs

Конфигурация

    if ([type] == "nginx_error_log") {
        grok {
            match => [ "message" , "(?<nginx_time>%{YEAR}/%{MONTHNUM}/%{MONTHDAY} %{TIME}) \[%{LOGLEVEL:log_level}\] %{GREEDYDATA:[application_data][nginx][message]}" ]
        }
        mutate {
            remove_field => [ "message" ]
        }
        date {
            match => [
                "[nginx_time]",
                "yyyy/dd/mm HH:mm:ss"
            ]
            target => "@application_timestamp"
        }
        mutate {
            remove_field => [ "nginx_time" ]
        }

    }

Построчный разбор

Фильтр что бы применять
    if ([type] == "nginx_error_log") {
Разбор с помощью grok
        grok {
            match => [ "message" , "(?<nginx_time>%{YEAR}/%{MONTHNUM}/%{MONTHDAY} %{TIME}) \[%{LOGLEVEL:log_level}\] %{GREEDYDATA:[application_data][nginx][message]}" ]
        }

Входные данные выглядят так:

2021/07/06 08:16:18 [notice] 1#1: start worker process 22

Другими словами эьл

  • Дата и время
  • Уровень сообщения
  • Текст сообщения

Конструкция

(?<nginx_time>%{YEAR}/%{MONTHNUM}/%{MONTHDAY} %{TIME}) 

означает что в результирующее поле nginx_time> записать то что попадет под выражение %{YEAR}/%{MONTHNUM}/%{MONTHDAY} %{TIME}
Далее - стандартная конструкция grok вида %{имя готового выражения:имя поля куда записать}

  • LOGLEVEL --> log_level
  • GREEDYDATA (остаток сообщения) --> [application_data][nginx][message] (вложенное поле)


Дебаг GROK: https://grokdebug.herokuapp.com

Удаление оригинальных полей
        mutate {
            remove_field => [ "message" ]
        }
Разбор даты
        date {
            match => [
                "[nginx_time]",
                "yyyy/dd/mm HH:mm:ss"
            ]
            target => "@application_timestamp"
        }
Удаление ненужного поля
        mutate {
            remove_field => [ "nginx_time" ]
        }

Filter для ruby application logs

Конфигурация

    if ([ruby_application_logs] == "true") {
        mutate {
            id => "[Meaningful label for your project, not repeated in any other config files] remove ANSI color codes"
            gsub => ["message", "\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]", ""]
        }

        grok {
            match => [ "message" , "%{WORD:[application_data][ruby][log_level_short]}, \[%{TIMESTAMP_ISO8601:[application_data][ruby][timestamp]} \#%{NUMBER}\] %{WORD:log_level} -- %{GREEDYDATA:[application_data][ruby][log_message]}" ]
        }

        mutate {
            remove_field => [ "message" ]
        }

        date {
            match => [
                "[application_data][ruby][timestamp]",
                "MMM dd yyyy HH:mm:ss",
                "MMM  d yyyy HH:mm:ss",
                "ISO8601"
            ]
            target => "@application_timestamp"
        }

    }

    mutate {
        uppercase => [ "log_level" ]
    }

}

Построчный разбор

Фильтрация

    if ([ruby_application_logs] == "true") {
Удаление цветных кодов
        mutate {
            id => "[Meaningful label for your project, not repeated in any other config files] remove ANSI color codes"
            gsub => ["message", "\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]", ""]
        }
Разбор с помощью grok
        grok {
            match => [ "message" , "%{WORD:[application_data][ruby][log_level_short]}, \[%{TIMESTAMP_ISO8601:[application_data][ruby][timestamp]} \#%{NUMBER}\] %{WORD:log_level} -- %{GREEDYDATA:[application_data][ruby][log_message]}" ]
        }

Поле message разбирается на поля

  • [application_data][ruby][log_level_short] - короткое однобуквенное обозначение уровня лога (D - Debug, E - Error и т.д.)
  • [application_data][ruby][timestamp] - время
  • log_level - уровень лога, словом
  • [application_data][ruby][log_message] - само сообщение до колнца строки
Удаление ненужного поля
        mutate {
            remove_field => [ "message" ]
        }
Получение даты
        date {
            match => [
                "[application_data][ruby][timestamp]",
                "MMM dd yyyy HH:mm:ss",
                "MMM  d yyyy HH:mm:ss",
                "ISO8601"
            ]
            target => "@application_timestamp"
        }
Преобразование к заглавным буквам

нужно так как приложения могут писать уровень по-разному - 'Error', 'error', 'ERROR'

    mutate {
        uppercase => [ "log_level" ]
    }

TODO Замена 'Err' -> Error и т.п. если найдется

Output

Общая для всех логов часть

Общий получатель логов - Elasticsearch

output {
    elasticsearch {
        hosts => ["localhost:9200"]
        index => "application-logs-%{[host][name]}-%{+YYYY.MM.dd}"
        document_type => "nginx_logs"
    }
    ...
    пропущены отладочные получатели
    ...
}

Отладка

Для того что б удобно смотреть на результат работы фильтров и преобразований самый простой способ - писать логи в файл.

Для отладки для nginx access logs

#    if ([type] == "nginx_access_log_json") {
#        file {
#            flush_interval => 5
#                gzip => false
#                path => "/var/log/logstash/nginx-%{[host][name]}-%{+YYYY.MM.dd}.log"
#        }
#    }

Для отладки для nginx error logs

#    if ([type] == "nginx_error_log") {
#        file {
#            flush_interval => 5
#                gzip => false
#                path => "/var/log/logstash/nginx-errors-%{[host][name]}-%{+YYYY.MM.dd}.log"
#        }
#    }

Для отладки для ruby application logs

#
#    if ([ruby_application_logs] == "true") {
#        file {
#            flush_interval => 5
#                gzip => false
#                path => "/var/log/logstash/ruby-%{[host][name]}-%{+YYYY.MM.dd}.log"
#        }
#    }

Elasticsearch-Curator

Установка

wget -qO - https://packages.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
echo 'deb [arch=amd64] https://packages.elastic.co/curator/5/debian stable main' > /etc/apt/sources.list.d/curator.list
apt update
apt-cache policy elasticsearch-curator
elasticsearch-curator:
  Installed: (none)
  Candidate: 5.8.4
  Version table:
     5.8.4 500
        500 https://packages.elastic.co/curator/5/debian stable/main amd64 Packages
apt install elasticsearch-curator

Конфигурация

mkdir /etc/elasticsearch-curator

client.yaml

---
client:
  hosts:
    - 127.0.0.1
  port: 9200
  url_prefix:
  use_ssl: False
  certificate:
  client_cert:
  client_key:
  ssl_no_validate: False
  username:
  password:
  timeout: 30
  master_only: False

logging:
  loglevel: INFO
  logfile:
  logformat: default
  blacklist: ['elasticsearch', 'urllib3']

actions.yaml

actions:
  1:
    action: delete_indices
    description: >-
      Delete indices older than 30 days (based on index name), for network-
      prefixed indices. Ignore the error if the filter does not result in an
      actionable list of indices (ignore_empty_list) and exit cleanly.
    options:
      ignore_empty_list: True
      disable_action: False
    filters:
    - filtertype: pattern
      kind: prefix
      value: application-logs-
    - filtertype: age
      source: name
      direction: older
      timestring: '%Y.%m.%d'
      unit: days
      unit_count: 30

Тестирование

Для теста ставим unit_count: 1 и проверяем с опцией --dry-run

+ /usr/bin/curator --dry-run --config /etc/elasticsearch-curator/client.yaml /etc/elasticsearch-curator/actions.yaml
2021-08-11 12:42:52,788 INFO      Preparing Action ID: 1, "delete_indices"
2021-08-11 12:42:52,788 INFO      Creating client object and testing connection
2021-08-11 12:42:52,791 INFO      Instantiating client object
2021-08-11 12:42:52,791 INFO      Testing client connectivity
/opt/python/3.9.4/lib/python3.9/site-packages/elasticsearch/connection/base.py:200: ElasticsearchWarning: Elasticsearch built-in security features are not enabled. Without authentication, your cluster could be accessible to anyone. See https://www.elastic.co/guide/en/elasticsearch/reference/7.14/security-minimal-setup.html to enable security.
...
/opt/python/3.9.4/lib/python3.9/site-packages/elasticsearch/connection/base.py:200: ElasticsearchWarning: this request accesses system indices: [.apm-agent-configuration, .apm-custom-link, .async-search, .kibana_7.14.0_001, .kibana_task_manager_7.14.0_001, .tasks], but in a future major version, direct access to system indices will be prevented by default
2021-08-11 12:42:52,854 INFO      DRY-RUN MODE.  No changes will be made.
2021-08-11 12:42:52,854 INFO      (CLOSED) indices may be shown that may not be acted on by action "delete_indices".
2021-08-11 12:42:52,854 INFO      DRY-RUN: delete_indices: application-logs-env1-2021.08.09 with arguments: {}
2021-08-11 12:42:52,854 INFO      DRY-RUN: delete_indices: application-logs-env1-2021.08.10 with arguments: {}
2021-08-11 12:42:52,854 INFO      DRY-RUN: delete_indices: application-logs-elk.domain.tld-2021.08.09 with arguments: {}
2021-08-11 12:42:52,855 INFO      DRY-RUN: delete_indices: application-logs-elk.doamin.tld-2021.08.10 with arguments: {}
2021-08-11 12:42:52,855 INFO      Action ID: 1, "delete_indices" completed.
2021-08-11 12:42:52,855 INFO      Job completed.

Индексы для удаления найдены

Задание Cron

Надо передлать на systemd-timer но лень

/etc/cron.daily/curator

#!/bin/bash

/usr/bin/curator \
    --config \
        /etc/elasticsearch-curator/config.yaml \
        /etc/elasticsearch-curator/actions.yaml 2>&1 | logger  -t "elasticsearch_curator"

Kibana

Установка

Установка из того же репозитория что и Elasticsearch

sudo apt-get install kibana
systemctl start kibana
systemctl enable kibana

Конфиги

kibana

server.port: 5601
server.host: "127.0.0.1"
server.publicBaseUrl: "https://elk.domain .tld"
elasticsearch.hosts: ["http://localhost:9200"]
kibana.index: ".kibana"
i18n.locale: "en"

nginx

Дополнения

Docker Autodiscovery

Добавлено в конце - для сбора логов докеа файлбит умеет автодискавери Filebeat Формат логов немного отличается

    if ([docker_autodiscovery] == "true") {
        mutate {
            id => "[Meaningful label for your project, not repeated in any other config files] remove ANSI color codes for Docker"
            gsub => ["message", "\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]", ""]
        }

        grok {
            match => [ "message" , "%{TIMESTAMP_ISO8601:[application_data][ruby][timestamp]} %{NUMBER}%{GREEDYDATA:[application_data][ruby][log_message]}" ]
        }

        mutate {
            remove_field => [ "message" ]
        }

        date {
            match => [
                "[application_data][ruby][timestamp]",
                "MMM dd yyyy HH:mm:ss",
                "MMM  d yyyy HH:mm:ss",
                "ISO8601"
            ]
            target => "@application_timestamp"
        }
    }

Ссылки