Насколько замечательно 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)