ELK Stack
Данное практическое занятие посвящено базовому взаимодействию со стеком ELK.
Vagrant
Для работы будем использовать следующий Vagrantfile
:
Vagrant.configure("2") do |config|
config.vm.define "logging" do |c|
c.vm.provider "virtualbox" do |v|
v.cpus = 2
v.memory = 4096
end
c.vm.box = "ubuntu/lunar64"
c.vm.hostname = "logging"
c.vm.network "forwarded_port", guest: 8888, host: 8888
c.vm.network "forwarded_port", guest: 8889, host: 8889
c.vm.provision "shell", inline: <<-SHELL
apt-get update -q
apt-get install -yq docker.io docker-compose-v2
usermod -a -G docker vagrant
SHELL
end
end
Данная конфигурация установит на виртуальную машину docker и docker compose, с помощью которых в дальнейшем будут развернуты остальные компоненты.
Elasticsearch
Запустим elasticsearch, для этого зададим его конфигурацию elasticsearch.yml
:
cluster.name: "elasticsearch"
network.host: localhost
И compose.yaml
:
version: '3'
services:
elasticsearch:
image: elasticsearch:7.9.1
container_name: elasticsearch
ports:
- "8889:9200"
- "9300:9300"
volumes:
- test_data:/usr/share/elasticsearch/data/
- ./elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
environment:
- discovery.type=single-node
- http.host=0.0.0.0
- transport.host=0.0.0.0
- xpack.security.enabled=false
- xpack.monitoring.enabled=false
- cluster.name=elasticsearch
- bootstrap.memory_lock=true
- ES_JAVA_OPTS=-Xms256m -Xmx256m
networks:
- elk
networks:
elk:
driver: bridge
volumes:
test_data:
После чего запустим:
$ docker compose up -d
[+] Running 3/3
✔ Network vagrant_elk Created 0.1s
✔ Volume "vagrant_test_data" Created 0.0s
✔ Container elasticsearch Started 1.5s
Теперь elasticsearch доступен по адресу localhost:8889
, можем взаимодействовать
с его api утилитой curl
. Для удобочитаемого вывода в api есть специальная группа
_cat
в которой можно получить различную информацию о кластере:
$ curl localhost:8889/_cat
=^.^=
/_cat/allocation
/_cat/shards
/_cat/shards/{index}
/_cat/master
/_cat/nodes
/_cat/tasks
/_cat/indices
/_cat/indices/{index}
/_cat/segments
/_cat/segments/{index}
/_cat/count
/_cat/count/{index}
/_cat/recovery
/_cat/recovery/{index}
/_cat/health
/_cat/pending_tasks
/_cat/aliases
/_cat/aliases/{alias}
/_cat/thread_pool
/_cat/thread_pool/{thread_pools}
/_cat/plugins
/_cat/fielddata
/_cat/fielddata/{fields}
/_cat/nodeattrs
/_cat/repositories
/_cat/snapshots/{repository}
/_cat/templates
/_cat/ml/anomaly_detectors
/_cat/ml/anomaly_detectors/{job_id}
/_cat/ml/trained_models
/_cat/ml/trained_models/{model_id}
/_cat/ml/datafeeds
/_cat/ml/datafeeds/{datafeed_id}
/_cat/ml/data_frame/analytics
/_cat/ml/data_frame/analytics/{id}
/_cat/transforms
/_cat/transforms/{transform_id}
$ curl localhost:8889/_cat/health
1711480438 19:13:58 elasticsearch green 1 1 0 0 0 0 0 0 - 100.0%
$ curl localhost:8889/_cat/health?v=true
epoch timestamp cluster status node.total node.data shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent
1711480449 19:14:09 elasticsearch green 1 1 0 0 0 0 0 0 - 100.0%
$ curl localhost:8889/_cat/health?help
epoch | t,time | seconds since 1970-01-01 00:00:00
timestamp | ts,hms,hhmmss | time in HH:MM:SS
cluster | cl | cluster name
status | st | health status
node.total | nt,nodeTotal | total number of nodes
node.data | nd,nodeData | number of nodes that can store data
shards | t,sh,shards.total,shardsTotal | total number of shards
pri | p,shards.primary,shardsPrimary | number of primary shards
relo | r,shards.relocating,shardsRelocating | number of relocating nodes
init | i,shards.initializing,shardsInitializing | number of initializing nodes
unassign | u,shards.unassigned,shardsUnassigned | number of unassigned shards
pending_tasks | pt,pendingTasks | number of pending tasks
max_task_wait_time | mtwt,maxTaskWaitTime | wait time of longest task pending
active_shards_percent | asp,activeShardsPercent | active number of shards in percent
$ curl localhost:8889/_cat/indices?v=true
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
Как видно, на текущий момент индексы отсутствуют. Создадим новый индекс:
$ curl -XPUT localhost:8889/test
{"acknowledged":true,"shards_acknowledged":true,"index":"test"}
$ curl localhost:8889/_cat/indices?v=true
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
yellow open test ehKCGlmsRMG9a3ZC1nf13g 1 1 0 0 208b 208b
$ curl -s localhost:8889/test/_search | jq
{
"took": 5,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 0,
"relation": "eq"
},
"max_score": null,
"hits": []
}
}
Сейчас он пуст, добавим в него документ:
$ curl -sH 'Content-Type: application/json' localhost:8889/test/_doc/ -d '{"message":"hello"}' | jq
{
"_index": "test",
"_type": "_doc",
"_id": "Us0-fI4BJUxYkdJGSOu-",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 1
}
$ curl -s localhost:8889/test/_search | jq
{
"took": 1045,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 1,
"hits": [
{
"_index": "test",
"_type": "_doc",
"_id": "Us0-fI4BJUxYkdJGSOu-",
"_score": 1,
"_source": {
"message": "hello"
}
}
]
}
}
Поиск без аргументов выводит все документы в индексе. Добавим еще один документ
и попробуем сделать поисковый запрос с помощью параметра q
:
$ curl -sH 'Content-Type: application/json' localhost:8889/test/_doc/ -d '{"message":"hello world"}' | jq
{
"_index": "test",
"_type": "_doc",
"_id": "Zs1NfI4BJUxYkdJGDOtc",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 1,
"_primary_term": 1
}
$ curl -s localhost:8889/test/_search?q="hello" | jq
{
"took": 6,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 0.6931471,
"hits": [
{
"_index": "test",
"_type": "_doc",
"_id": "Us0-fI4BJUxYkdJGSOu-",
"_score": 0.6931471,
"_source": {
"message": "hello"
}
},
{
"_index": "test",
"_type": "_doc",
"_id": "Zs1NfI4BJUxYkdJGDOtc",
"_score": 0.160443,
"_source": {
"message": "hello world"
}
}
]
}
}
$ curl -s localhost:8889/test/_search?q="world" | jq
{
"took": 6,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 0.60996956,
"hits": [
{
"_index": "test",
"_type": "_doc",
"_id": "Zs1NfI4BJUxYkdJGDOtc",
"_score": 0.60996956,
"_source": {
"message": "hello world"
}
}
]
}
}
$ curl -s 'localhost:8889/test/_search?q=message.keyword:"hello"' | jq
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 0.6931471,
"hits": [
{
"_index": "test",
"_type": "_doc",
"_id": "Us0-fI4BJUxYkdJGSOu-",
"_score": 0.6931471,
"_source": {
"message": "hello"
}
}
]
}
}
$ curl -s localhost:8889/test/_search?q=/.*world/ | jq
{
"took": 14,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 1,
"hits": [
{
"_index": "test",
"_type": "_doc",
"_id": "Zs1NfI4BJUxYkdJGDOtc",
"_score": 1,
"_source": {
"message": "hello world"
}
}
]
}
}
Как видно elasticsearch позволяет делать запросы различного вида для поиска в индексе. В конце удалим наш индекс:
$ curl -XDELETE localhost:8889/test
{"acknowledged":true}
$ curl localhost:8889/_cat/indices?v=true
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
Logstash
Развернем также logstash, который позволяет принимать, обрабатывать и записывать
данные в elasticsearch. Для этого определим два конфигурационных файла
logstash.yml
и logstash.conf
:
http.host: 0.0.0.0
xpack.monitoring.elasticsearch.hosts: ["http://elasticsearch:9200"]
input {
file {
path => ["/var/lib/docker/containers/*/*.log"]
}
}
filter {
}
output {
elasticsearch {
hosts => "http://elasticsearch:9200"
index => "test-logs-%{+YYYY.MM.DD}"
}
}
Данная конфигурация позволит считывать логи контейнеров и отправлять их elastic. Но для
этого потребуется изменить конфигурацию docker демона /etc/docker/daemon.json
:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3",
"tag": "{{.ImageName}}/{{.Name}}"
}
}
После чего потребуется перезапустить его командой:
sudo systemctl restart docker
Теперь опишем сервис logstash в compose.yaml
:
version: '3'
services:
elasticsearch:
image: elasticsearch:7.9.1
container_name: elasticsearch
ports:
- "8889:9200"
- "9300:9300"
volumes:
- test_data:/usr/share/elasticsearch/data/
- ./elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
environment:
- discovery.type=single-node
- http.host=0.0.0.0
- transport.host=0.0.0.0
- xpack.security.enabled=false
- xpack.monitoring.enabled=false
- cluster.name=elasticsearch
- bootstrap.memory_lock=true
- ES_JAVA_OPTS=-Xms256m -Xmx256m
networks:
- elk
logstash:
image: logstash:7.9.1
container_name: logstash
user: "0"
ports:
- "5044:5044/udp"
- "9600:9600"
volumes:
- ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf
- ./logstash.yml:/usr/share/logstash/config/logstash.yml
- ls_data:/usr/share/logstash/data
- /var/lib/docker/containers:/var/lib/docker/containers
networks:
- elk
depends_on:
- elasticsearch
networks:
elk:
driver: bridge
volumes:
test_data:
ls_data:
И запустим:
$ docker compose up -d
[+] Running 3/3
✔ Volume "vagrant_ls_data" Created 0.0s
✔ Container elasticsearch Started 0.3s
✔ Container logstash Started 0.6s
После запуска logstash создаст свой индекс и начнет писать в него сообщения:
$ curl localhost:8889/_cat/indices?v=true
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
yellow open test-logs-2024.03.86 2hpFb0ffRgmgtnyMI4_plw 1 1 4 0 10.2kb 10.2kb
$ curl -s localhost:8889/test-logs-2024.03.86/_search?size=1 | jq
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 4,
"relation": "eq"
},
"max_score": 1,
"hits": [
{
"_index": "test-logs-2024.03.86",
"_type": "_doc",
"_id": "4ViDfI4BPacsZSlTAf5s",
"_score": 1,
"_source": {
"message": "{\"log\":\"[2024-03-26T20:45:42,599][INFO ][logstash.agent ] Successfully started Logstash API endpoint {:port=\\u003e9600}\\n\",\"stream\":\"stdout\",\"attrs\":{\"tag\":\"logstash:7.9.1/logstash\"},\"time\":\"2024-03-26T20:45:42.607986242Z\"}",
"@timestamp": "2024-03-26T20:45:43.682Z",
"path": "/var/lib/docker/containers/34fa05cc4d33e34a8ef0b385419f3714773a6e12b4d5a4919a4aebf90a13155a/34fa05cc4d33e34a8ef0b385419f3714773a6e12b4d5a4919a4aebf90a13155a-json.log",
"host": "34fa05cc4d33",
"@version": "1"
}
}
]
}
}
Можем запустить свой контейнер, который будет только выводить введенный текст:
$ docker run -it --name alpine alpine sh -c 'cat >/dev/null'
Unable to find image 'alpine:latest' locally
latest: Pulling from library/alpine
4abcf2066143: Pull complete
Digest: sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b
Status: Downloaded newer image for alpine:latest
hello
После переноса строки можно завершить ввод комбинацией ctrl+d
и попытаться найти
наш текст в индексе:
$ curl -s 'localhost:8889/test-logs-2024.03.86/_search?size=1&sort=@timestamp:desc' | jq
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 5,
"relation": "eq"
},
"max_score": null,
"hits": [
{
"_index": "test-logs-2024.03.86",
"_type": "_doc",
"_id": "6FiKfI4BPacsZSlT9v5L",
"_score": null,
"_source": {
"message": "{\"log\":\"hello\\r\\n\",\"stream\":\"stdout\",\"attrs\":{\"tag\":\"alpine/alpine\"},\"time\":\"2024-03-26T20:54:24.873049246Z\"}",
"@timestamp": "2024-03-26T20:54:25.503Z",
"path": "/var/lib/docker/containers/a7320f962f5b0a7a087831b685b38517a1ef8c84d64ce6018d160b54bbdeb07f/a7320f962f5b0a7a087831b685b38517a1ef8c84d64ce6018d160b54bbdeb07f-json.log",
"host": "34fa05cc4d33",
"@version": "1"
},
"sort": [
1711486465503
]
}
]
}
}
Помимо получения из файла и отправки в индекс в конфигурации logstash можно также
задать обработку данных в параметре filter
. Как видно в message
нашего документа
содержится текст в формате json
, в котором по ключу attrs.tag
хранится информация
об образе и имени контейнера. Попробуем достать эти данные и положить в отдельные поля,
для этого дополним logstash.conf
:
input {
file {
path => ["/var/lib/docker/containers/*/*.log"]
}
}
filter {
json {
source => "message"
}
mutate {
split => { "[attrs][tag]" => "/" }
}
mutate {
add_field => {
"image" => "%{[attrs][tag][0]}"
"container" => "%{[attrs][tag][1]}"
}
remove_field => ["attrs", "message"]
}
}
output {
elasticsearch {
hosts => "http://elasticsearch:9200"
index => "test-logs-%{+YYYY.MM.DD}"
}
}
После чего перезапустим контейнеры:
$ docker compose up -d --force-recreate
[+] Running 2/2
✔ Container elasticsearch Started 2.1s
✔ Container logstash Started 2.0s
Повторно запустим тестовый контейнер:
$ docker rm -f alpine
alpine
$ docker run -it --name alpine alpine sh -c 'cat >/dev/null'
test 123
И посмотрим как теперь выглядит документ:
$ curl -s 'localhost:8889/test-logs-2024.03.86/_search?q=container:alpine' | jq
{
"took": 22,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 1.2039728,
"hits": [
{
"_index": "test-logs-2024.03.86",
"_type": "_doc",
"_id": "ctSXfI4BBZ7dkKnP6S3b",
"_score": 1.2039728,
"_source": {
"image": "alpine",
"@timestamp": "2024-03-26T21:08:34.285Z",
"log": "test 123\r\n",
"path": "/var/lib/docker/containers/ecedf32cc76a9e3d7843bee34f86a3873fd1b21a0cb9ad41689308b1051b984d/ecedf32cc76a9e3d7843bee34f86a3873fd1b21a0cb9ad41689308b1051b984d-json.log",
"stream": "stdout",
"@version": "1",
"host": "12b43b1d6336",
"time": "2024-03-26T21:08:33.97032219Z",
"container": "alpine"
}
}
]
}
}
$ curl -s 'localhost:8889/test-logs-2024.03.86/_search?q=container:logstash&size=1' | jq
{
"took": 2,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 1.2039728,
"hits": [
{
"_index": "test-logs-2024.03.86",
"_type": "_doc",
"_id": "b9SWfI4BBZ7dkKnP7y0b",
"_score": 1.2039728,
"_source": {
"image": "logstash:7.9.1",
"@timestamp": "2024-03-26T21:07:30.049Z",
"log": "[2024-03-26T21:07:28,894][INFO ][logstash.agent ] Successfully started Logstash API endpoint {:port=>9600}\n",
"path": "/var/lib/docker/containers/12b43b1d6336d6e88c82405cb6e22d04a2f79750386d811af4a80f0304af225e/12b43b1d6336d6e88c82405cb6e22d04a2f79750386d811af4a80f0304af225e-json.log",
"stream": "stdout",
"@version": "1",
"host": "12b43b1d6336",
"time": "2024-03-26T21:07:28.894641313Z",
"container": "logstash"
}
}
]
}
}
Kibana
Для удобной визуализации логов в elasticsearch воспользуемся инструментом kibana.
Создадим конфигурацию kibana.yml
:
server.name: kibana
server.host: "0"
elasticsearch.hosts: [ "http://elasticsearch:9200" ]
monitoring.ui.container.elasticsearch.enabled: true
И дополним наш compose.yaml
:
version: '3'
services:
elasticsearch:
image: elasticsearch:7.9.1
container_name: elasticsearch
ports:
- "8889:9200"
- "9300:9300"
volumes:
- test_data:/usr/share/elasticsearch/data/
- ./elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
environment:
- discovery.type=single-node
- http.host=0.0.0.0
- transport.host=0.0.0.0
- xpack.security.enabled=false
- xpack.monitoring.enabled=false
- cluster.name=elasticsearch
- bootstrap.memory_lock=true
- ES_JAVA_OPTS=-Xms256m -Xmx256m
networks:
- elk
logstash:
image: logstash:7.9.1
container_name: logstash
user: "0"
ports:
- "5044:5044/udp"
- "9600:9600"
volumes:
- ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf
- ./logstash.yml:/usr/share/logstash/config/logstash.yml
- ls_data:/usr/share/logstash/data
- /var/lib/docker/containers:/var/lib/docker/containers
networks:
- elk
depends_on:
- elasticsearch
kibana:
image: kibana:7.9.1
container_name: kibana
ports:
- "8888:5601"
volumes:
- ./kibana.yml:/usr/share/kibana/config/kibana.yml
- kb_data:/usr/share/kibana/data
networks:
- elk
depends_on:
- elasticsearch
networks:
elk:
driver: bridge
volumes:
test_data:
ls_data:
kb_data:
После чего можем запустить:
$ docker compose up -d
[+] Running 3/3
✔ Container elasticsearch Running 0.0s
✔ Container logstash Running 0.0s
✔ Container kibana Started 0.3s
Kibana будет доступна по адресу localhost:8888, а по адресу localhost:8888/app/discover можно будет сделать поиск наших логов. Для этого потребуется добавить паттерн для нашего индекса:
И можно будет вернуться на страницу /app/discover:
В левой панели можно выбрать поля для отображения:
Добавим поля container
и log
:
Поиск можно осуществлять с помощью KQL:
Различные визуализации можно создавать на странице /app/visualize. Добавим визуализацию в виде pie chart:
После чего в правой панели зададим метрику Count
:
И разбиение по бакетам:
Нажав кнопку Update
увидим визуализацию, которая отображает соотношение логов
по разным контейнерам: