Насколько замечательно Grafana, Graphite и Prometheus справляются с численными данными мониторинга, вроде загрузки процессора по времени, настолько же они бесполезны для работы с логами. А ведь с теми приходится работать даже чаще, чем с метриками.
Инструментов для работы с логами тоже навалом, но сегодня мы посмотрим в сторону ELK стека от Elastic. А именно: Elasticsearch, Logstash и Kibana — хранилище/поисковик, обработчик данных и тулза для их визуализации. А начнём, естественно, с первой буквы этого трёх-буквенного алфавита — «E».
Что такое Elasticsearch
Elasticsearch — это быстрый, горизонтально масштабируемый и очень бесплатный гибрид NoSQL базы данных и гугла для неё. С миром он общается через HTTP API и через него уже получает на вход JSON документы для индексации и хранения. Хранение, правда, можно отключить, и тогда останется только поисковик, возвращающий айдишки когда-то проиндексированных документов.
Установка
Так как Elasticsearch написан на Java, устанавливается он очень просто: качаем архив, распаковываем и запускаем bin/elasticsearch
. Правда, запустить его через Docker будет еще проще: docker run -d -p9200:9200 elasticsearch
. Всё общение будет проходить через порт 9200, так что самое время в него постучаться.
Осматриваемся
Вэбинары и официальная документация обычно показывают демки, используя еще один продукт от Elastic — Kibana. Но так речь идёт об обычном HTTP и JSON, отправлять и получать запросы можно чем угодно. Наверное, самый простой готовый инструмент — консоль и curl
.
Итак, у нас есть порт 9200. Попробуем его пнуть просто так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$ curl 127.0.0.1:9200 #{ # "name" : "e-wGdWV", # "cluster_name" : "elasticsearch", # "cluster_uuid" : "ZxPcxDlFTSu68zpY9foYiw", # "version" : { # "number" : "5.2.0", # "build_hash" : "24e05b9", # "build_date" : "2017-01-24T19:52:35.800Z", # "build_snapshot" : false, # "lucene_version" : "6.4.0" # }, # "tagline" : "You Know, for Search" #} |
Неплохо. Номер версии в середине ответа и шутка юмора внизу — запрос определенно того стоил.
Есть и другие полезные запросы, для которых в индексе не обязательно должны быть данные. Например, можно проверить статус узла:
1 2 3 |
$ curl 127.0.0.1:9200/_cat/health?v #epoch timestamp cluster status node.total node.data shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent #1486360312 05:51:52 elasticsearch yellow 1 1 5 5 0 0 5 0 - 50.0% |
Или получить список индексов:
1 2 |
$ curl 127.00.1:9200/_cat/indices?v #health status index uuid pri rep docs.count docs.deleted store.size pri.store.size |
Так как установка свежая, то, разумеется, индексов в ней никаких еще нет. Но мы как-то внезапно начали оперировать специфическими терминами, вроде узла и индекса, так что стоит сначала разобраться с терминологией.
Терминология Elasticsearch
Итак, мы уже упомянули узел (node), что на самом деле означает отдельно взятый процесс elasticsearch с его данными и настройками. Даже один узел образует кластер. Но если узлов в кластере несколько, то включённый по умолчанию шардинг (дробление) индексов и репликация шардов (shards) резко повысит и выживаемость индексов за счёт избыточности, и скорость запросов к ним за счёт параллелизации. Похожий подход, кстати, работает и в Kafka, только там шарды называются разделами.
Сам по себе индекс — это просто коллекция документов. В пределах кластера их может быть уйма. Внутри индекса документы можно организовать по типам — произвольным именам, описывающим похожие по структуре документы. Наконец, сам документ — это просто JSON. Обычный скучный JSON.
Создание, чтение, обновление и удаление
С двумя абзацами теории разобрались, самое время что-нибудь сделать.
Создание
Добавить документ в elasticsearch так же просто, как и сделать HTTP POST. Ну один в один:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$ curl -X POST 127.0.0.1:9200/monitor/logs?pretty -d '{ "kind": "info", "message": "The server is up and running" }' #{ # "_index" : "monitor", # "_type" : "logs", # "_id" : "AVoWblBE6fU5oFCNC7jY", # "_version" : 1, # "result" : "created", # "_shards" : { # "total" : 2, # "successful" : 1, # "failed" : 0 # }, # "created" : true #} |
Мы отправили документ { "kind": "info", "message": "..."}
в индекс под названием monitor
, и тип с названием logs
. Ни индекса, ни типа до этого не существовало, и elasticsearch создал их, взяв названия из URL. Вдобавок, он разродился ответным JSON, в который положил сгенерированный идентификатор документа (_id
) и немного других деталей. Кстати, можно задать и свой _id, просто заменив POST на PUT и передав идентификатор в URL. Например, вот так -X PUT monitor/logs/42
. Слово ?pretty
в строке запроса просто включает форматирование ответов.
Так как добавлять документы по одному то еще удовольствие, можно сразу добвить целую пачку через bulk запрос:
1 2 3 4 5 6 |
$ curl -X POST 127.0.0.1:9200/monitor/logs/_bulk -d ' { "index": {}} { "kind" : "warn", "message": "Using 90% of memory" } { "index": {}} { "kind": "err", "message": "OutOfMemoryException: Epic fail has just happened" } ' |
В bulk запросе на каждый документ идёт 2 JSON фрагмента. Один показывает, что именно нужно делать с документом (в нашем случае «index»). Второй фрагмент — сам документ.
Чтение
Теперь, раз уж у нас уже есть что-то в индексе, можно запросить что-нибудь назад. Если сделать пустой поиск, то мы получим в ответ всё, что успели добавить:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
curl 127.0.0.1:9200/monitor/_search?pretty #{ # ......... # "hits" : { # "total" : 3, # "max_score" : 1.0, # "hits" : [ # { # "_index" : "monitor", # "_type" : "logs", # "_id" : "AVoWe_7d6fU5oFCNC7jb", # "_score" : 1.0, # "_source" : { # "kind" : "err", # "message" : "OutOfMemoryException: Epic fail has just happened" # } # }, # { # "_index" : "monitor", # "_type" : "logs", # "_id" : "AVoWe_7d6fU5oFCNC7ja", # "_score" : 1.0, # "_source" : { # "kind" : "warn", # "message" : "Using 90% of memory" # } # }, # { # "_index" : "monitor", # "_type" : "logs", # "_id" : "AVoWblBE6fU5oFCNC7jY", # "_score" : 1.0, # "_source" : { # "kind" : "info", # "message" : "The server is up and running" # } # } # ] # } #} |
Но можно запросить конкретный документ по его ID:
1 2 3 4 5 6 7 8 |
curl 127.0.0.1:9200/monitor/logs/AVoWblBE6fU5oFCNC7jY?pretty #{ # ... # "_source" : { # "kind" : "info", # "message" : "The server is up and running" # } #} |
Обновление
По аналогии, зная идентификатор документа, можно его обновить. Так как моё начальство нервно относится к фразам в логах вроде «Epic fail has just happened», его стоит заменить на что-нибудь другое:
1 2 3 4 |
$ curl -X POST 127.0.0.1:9200/monitor/logs/AVoWe_7d6fU5oFCNC7jb -d ' { "kind": "err", "message": "OutOfMemoryException: The server process used all available memory" }' |
Но на самом деле в этом случае elasticsearch не обновляет документ, а просто заменяет его на другой, сохраняя прежний ID.
Удаление
Чтобы удалить что-нибудь есть HTTP DELETE. Например,
curl -X DELETE 127.0.0.1:9200/monitor/logs/AVoWe_7d6fU5oFCNC7jb
Поиск
Но добавлять и получать назад JSON умеют многие NoSQL базы. Самая мощная фишка в elasticsearch — это, конечно, поиск. Есть два способа, чтобы искать документы по индексу: REST Request API для простых поисковых запросов, и более серьёзный Query DSL.
REST Request API просто добавляет дополнительный параметр в URL запроса, вроде такого:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
$ curl -s 127.0.0.1:9200/monitor/_search?q=memory | json_pp #{ .... # "hits" : { # "hits" : [ # { # "_id" : "AVoWe_7d6fU5oFCNC7ja", # "_source" : { # "kind" : "warn", # "message" : "Using 90% of memory" # }, # .... # "_score" : 0.2824934 # }, # { # "_id" : "AVoWe_7d6fU5oFCNC7jb", # "_source" : { # "kind" : "err", # "message" : "OutOfMemoryException: The server process used all available memory" # }, # ... # "_score" : 0.27233246 # } # ], # "total" : 2, # "max_score" : 0.2824934 # ... #} |
Но через один ‘q='-
параметр сложный запрос не соберешь. В Query DSL, с другой стороны, можно и комбинировать ключевые слова, и собирать булевы выражения, добавлять фильтры — много чего можно сделать, чтобы найти что-то конкретное.
Query DSL поиск это всё тот же HTTP GET запрос, но с немного более замороченным синтаксисом. Например, если мы хотим найти логи, которые так или иначе касаются памяти, но при этом не критичны, можно собрать такой запрос:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
$ curl -s 127.0.0.1:9200/monitor/_search -d ' { "query": { "bool": { "must": [ { "match": { "kind":"info warn" }}, { "match": { "message":"memory" }} ] } } }' | json_pp #{ # ... # "hits" : { # "total" : 1, # "hits" : [ # { # "_type" : "logs", # "_index" : "monitor", # "_source" : { # "message" : "Using 90% of memory", # "kind" : "warn" # }, # "_score" : 0.5753642, # "_id" : "AVoWe_7d6fU5oFCNC7ja" # } # ], # "max_score" : 0.5753642 # } #} |
Агрегирование
В довесок к поиску данные можно еще и агрегировать. Агрегирование само по себе огромный топик, но в качестве примера можно просто посчитать, сколько логов у нас скопилось по категориям:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
curl -s 127.0.0.1:9200/monitor/_search -d ' { "size": 0, "aggs": { "group_by_kind": { "terms": { "field" : "kind.keyword" } } } }' | json_pp #{ # "aggregations" : { # "group_by_kind" : { # "sum_other_doc_count" : 0, # "buckets" : [ # { # "key" : "err", # "doc_count" : 1 # }, # { # "doc_count" : 1, # "key" : "info" # }, # { # "key" : "warn", # "doc_count" : 1 # } # ], # ... #} |
Так как _search
URL делает и поиск и агрегацию, а ключевые слова для поиска мы не предоставили, в параметры пришлось добавить "size": 0
, чтобы запрос не вернул всё коллекцию документов в довесок к статистике.
Завершение
Обычно в этом месте я пишу «сегодня мы едва прошлись по X», но elasticsearch настолько огромный, что даже слово «едва» ошибочно создаёт иллюзию объёма. Обновление документов через отправку скрипта, схемы полей, фильтры, сложные запросы и агрегирование, работа в кластерах, анализаторы документов и куча других вещей — мы их вообще не затронули.
Но и того, что мы посмотрели, надеюсь, должно хватить, чтобы понять: elasticsearch — это вменяемо простой поисковый движок с понятным API и миллионом фич, которые нужно гуглить и гуглить. В следующий раз мы посмотрим, как заполнять его текстовыми данными мониторинга при помощи Logstash.
спасибо за пост.
просто, понятно и сразу можно начинать.
может быть вы знаете, как сконфигурировать стандартные логи пайтона для добавления этих логов сразу в ElasticSearch?
Пожалуйста.
Насчёт питона и его логов, не скажу, что я в этом спец, но я бы пытался добавить соответствующий хэндлер (для elasticsearch или logstash) к стандартному питоновскому логгеру. Если гуглить что-то вроде «python logging handler elasticsearch» то сразу попадается что-то вроде такого — https://github.com/cmanaha/python-elasticsearch-logger. И там примеры кода очень похожие на то, что я бы ожидал увидеть:
from cmreslogging.handlers import CMRESHandler
handler = CMRESHandler(hosts=[{'host': 'localhost', 'port': 9200}],
auth_type=CMRESHandler.AuthType.NO_AUTH,
es_index_name="my_python_index")
log = logging.getLogger("PythonTest")
log.setLevel(logging.INFO)
log.addHandler(handler)