Opensearch, Logstash и dynamic mapping
У нас в Домклик огромное количество микросервисов, около 5000. Все они пишут какие‑то логи. В этой статье я хочу рассказать о том, как у нас настроен маппинг в индексах Opensearch и какие «фишки» мы используем, чтобы минимизировать работы по настройке маппинга.
Введение
Конечно же, логи не пишутся в один общий индекс. Для разделения мы используем понятие «продукты», то есть несколько сервисов объединяются в продукт. Всего у нас около 200 продуктов и 200 индексов. Мы не регулируем наименование и вложенность полей, только количество (не более 200 на индекс).
В Opensearch есть чудесная функция — динамический маппинг. Его суть в том, что Opensearch по умолчанию пытается типизировать поле как текст. Но есть и ложка дёгтя (даже половник), маппинг не работает для вложенных полей, если родительские поля не заданы явно как object
. Например, для поля host.name
потребуется такая конфигурация, иначе будет ошибка:
"mappings": {
"properties": {
"host": {
"type": "object"
}
}
}
Что делать, чтобы ненастраивать маппинг сотен полей в сотнях индексах руками, а также не маппить всё как текст, а получить хоть какую‑то типизацию автоматически? Вот вам пара фишек.
Фишка №1. logstash-filter-flatten_json
Фильтр для Logstash. Нашли его на github и чуть‑чуть доработали. Превращает поля JSON в «плоские», то есть добавляет в название поля всех его «родителей» через точку и выносит его на самый верхний уровень. Например, такой JSON:
{
"key_1": "value_1",
"key_3": {
"nested_key_1": "nested_value_1"
}
}
превращается в такой:
{
"key_1": "value_1",
"key_3.nested_key_1": "nested_value_1"
}
Если поле содержит массив, то значения в нём приводятся к строке и дальше вложенность не парсится. Если вам такое не подходит, то можно взять исходный плагин из оригинального репозитория, в котором значения массивов парсятся как отдельные поля. Этот плагин решает проблему с динамическим маппингом, гарантируя, что все поля будут проиндексированы как минимум как текст.
Фишка №2. Динамическое распознавание типов
Две функции динамического маппинга:
"mappings": {
"date_detection": true,
"dynamic_date_formats": ["yyyy-MM-dd HH:mm:ss.SSS 'MSK'"],
"numeric_detection": true
}
date_detection
— распознавание дат. В массиве dynamic_date_formats
можно указать любые форматы, подробнее описано в тут (в документации Opensearch этого нет, но есть в документации Elasticsearch).
numeric_detection
— распознавание чисел (long, float и т. д.)
Фишка №3. Очистка пустых полей
Бывает, что какое‑то поле приходит пустым или с прочерком. Прочерк считается текстом и проиндексировать такое поле как, например, число не получится. Для этого написали небольшой фильтр для Logstash на Ruby:
ruby {
code => "
Hash[event].each { |k, v|
if v.kind_of?(Array)
if v.empty? or v.include? '-'
event.remove(k)
end
else
if v == '-'
event.remove(k)
end
end
}
"
}
Это «лечит» логи, которые не попали бы в индекс из‑за неверной типизации.
Фишка №4. Индексирование поля с несколькими типами
Для построения дашбордов со статистикой требуется тип keyword. Но поля с таким типом плохо подходят для полнотекстового поиска. Решением будет индексировать их с двумя типами одновременно. Для этого добавляем блок fields
:
"mappings": {
"dynamic_templates": [{
"string_fields": {
"mapping": {
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
}
},
"match_mapping_type": "string",
"match": "*"
}
}]
}
Таким образом мы сможем и построить дашборд по таким полям, и использовать все функции полнотекстового поиска.
Подведем итоги
Благодаря этим «фишкам» мы не настраиваем около 90% индексов. Все поля в них автоматически распознаются и типизируются как текст, даты и числа. Для остальных 10% приходится настраивать небольшой маппинг руками. Например, если поле содержит IP‑адрес.
Правильная типизация позволяет делать дашборды со статистикой по логам, более гибкий поиск со сравнением чисел и дат, вхождение IP по маске и так далее.