LogstashExample1: различия между версиями
Sirmax (обсуждение | вклад) |
Sirmax (обсуждение | вклад) |
||
(не показана 151 промежуточная версия этого же участника) | |||
Строка 4: | Строка 4: | ||
[[Категория:Grok]] |
[[Категория:Grok]] |
||
[[Категория:nginx]] |
[[Категория:nginx]] |
||
+ | [[Категория:Filebeat]] |
||
+ | [[Категория:Elasticsearch]] |
||
+ | =Отдельные заметки= |
||
+ | Некоторые вещи были доработаны после написания основной части по опыту использования и вынесены в отдельные заметки |
||
+ | * Logstash DQF [[LogstashDLQ]] |
||
+ | |||
=Пример= |
=Пример= |
||
− | Это |
+ | Это дополнение статьи https://noname.com.ua/mediawiki/index.php?title=Logstash |
− | ==nginx== |
||
− | Отправка логов nginx в Elastic (один из вариантов) <BR> |
||
− | Тут рассматривается самый простой случай - когда Filebeat не делает преобразований |
||
− | Nginx умеет писать логи в Json |
||
+ | =Задача= |
||
+ | Задача - отправка логов от тестового приложения в 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"] |
||
Строка 76: | Строка 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: |
||
Строка 82: | Строка 100: | ||
- add_docker_metadata: ~ |
- add_docker_metadata: ~ |
||
- add_kubernetes_metadata: ~ |
- add_kubernetes_metadata: ~ |
||
− | logging.level: |
+ | 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 |
||
− | Минимальный конфиг - слушать Filebeat на порту 5400 (без сертификата - не будет работать) |
||
+ | <BR> Разбор лога на отдельные поля происходит на стороне Logstash. |
||
− | <BR> Результат - писать в файл без всяких преобразований |
||
+ | <PRE> |
||
+ | - 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 { |
||
Строка 108: | Строка 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": { |
||
− | }, |
||
− | " |
+ | "ruby": { |
− | " |
+ | "object": "ruby_object" |
− | }, |
+ | }, |
− | " |
+ | "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 сообщение попадет "как есть" |
||
+ | <BR> |
||
+ | С фильтром же результат будет |
||
+ | <PRE> |
||
+ | { |
||
+ | "application_data": { |
||
+ | "nginx": { |
||
+ | "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> |
||
+ | 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
Отдельные заметки
Некоторые вещи были доработаны после написания основной части по опыту использования и вынесены в отдельные заметки
- Logstash DQF LogstashDLQ
Пример
Это дополнение статьи 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
- Из-за размера вынесено в отдельную заметку: LogstashExample-nginx-config
Результат
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]
- Подробнее про разбор многострочных сообщений - FilebeatMultiline
- Оригинальная документация - https://www.elastic.co/guide/en/beats/filebeat/current/multiline-examples.html (но не слишком понятная для меня)
- TL;DR: использовать negate: true и match: after практически во всех случаях
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] - само сообщение до колнца строки
- Дебаг GROK: https://grokdebug.herokuapp.com (что б не забыть)
Удаление ненужного поля
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" } }