Практичне застосування ELK. Налаштовуємо logstash

Запровадження

Розгортаючи чергову систему, зіткнулися з необхідністю обробляти багато різноманітних логів. Як інструмент вибрали ELK. У цій статті йтиметься про наш досвід налаштування цього стека.

Не ставимо за мету описати всі його можливості, але хочемо сконцентруватися саме на вирішенні практичних завдань. Викликано це тим, що за наявності досить великої кількості документації та вже готових образів, підводного каміння досить багато, принаймні у нас вони виявилися.

Ми розгортали стек через docker-compose. Більше того, у нас був добре написаний docker-compose.yml, який дозволив нам практично без проблем підняти стек. І нам здавалося, що перемога вже близька, зараз трохи докрутимо під свої потреби і все.

На жаль, спроба доналаштувати систему на отримання та обробку логів від нашого додатку, з ходу не мала успіху. Тому ми вирішили, що варто вивчити кожен компонент окремо, а потім уже повернутися до їхніх зв'язків.

Отже, почали з logstash.

Оточення, розгортання, запуск Logstash у контейнері

Для розгортання використовуємо docker-compose, описані тут експерименти проводились на MacOS та Ubuntu 18.0.4.

Образ logstash, який був прописаний у нас у вихідному docker-compose.yml, це docker.elastic.co/logstash/logstash:6.3.2

Його ми використовуватимемо для експериментів.

Для запуску logstash ми написали окрему docker-compose.yml. Можна, звичайно, було з командного рядка образ запускати, але ж ми конкретне завдання вирішували, де у нас все з docker-compose запускається.

Коротко про конфігураційні файли

Як випливає з опису, logstash можна запускати як для одного каналу, у цьому випадку йому потрібно передати файл *.conf або для декількох каналів, в цьому випадку йому треба передати файл pipelines.yml, який, у свою чергу, посилатиметься на файли .conf для кожного каналу.
Ми пішли другим шляхом. Він нам здався більш універсальним та масштабованим. Тому ми створили pipelines.yml і зробили директорію pipelines, в яку будемо класти файли .conf для кожного каналу.

Усередині контейнера є ще один файл конфігурації - logstash.yml. Ми його не чіпаємо, використовуємо як є.

Отже, структура наших каталогів:

Практичне застосування ELK. Налаштовуємо logstash

Для отримання вхідних даних поки вважаємо, що це tcp портом 5046, а для виводу будемо використовувати stdout.

Ось така проста конфігурація для першого запуску. Бо ж початкове завдання — запустити.

Отже, у нас є такий docker-compose.yml

version: '3'

networks:
  elk:

volumes:
  elasticsearch:
    driver: local

services:

  logstash:
    container_name: logstash_one_channel
    image: docker.elastic.co/logstash/logstash:6.3.2
    networks:
      	- elk
    ports:
      	- 5046:5046
    volumes:
      	- ./config/pipelines.yml:/usr/share/logstash/config/pipelines.yml:ro
	- ./config/pipelines:/usr/share/logstash/config/pipelines:ro

Що ми тут бачимо?

  1. Networks і volumes були взяті з вихідного docker-compose.yml (то де повністю стек запускається) і думаю, що сильно тут на загальну картинку не впливають.
  2. Ми створюємо один сервіс (services) logstash з образу docker.elastic.co/logstash/logstash:6.3.2 і присвоюємо йому ім'я logstash_one_channel.
  3. Ми прокидаємо всередину контейнера порт 5046, на такий самий внутрішній порт.
  4. Ми відображаємо наш файл налаштування каналів ./config/pipelines.yml на файл /usr/share/logstash/config/pipelines.yml всередині контейнера, звідки його підхопить logstash і робимо його read-only, просто про всяк випадок.
  5. Ми відображаємо директорію ./config/pipelines, де у нас лежать файли з налаштуваннями каналів, в директорію /usr/share/logstash/config/pipelines і теж робимо її read-only.

Практичне застосування ELK. Налаштовуємо logstash

Файл pipelines.yml

- pipeline.id: HABR
  pipeline.workers: 1
  pipeline.batch.size: 1
  path.config: "./config/pipelines/habr_pipeline.conf"

Тут описаний один канал з ідентифікатором HABR та шлях до конфігураційного файлу.

І нарешті файл "./config/pipelines/habr_pipeline.conf"

input {
  tcp {
    port => "5046"
   }
  }
filter {
  mutate {
    add_field => [ "habra_field", "Hello Habr" ]
    }
  }
output {
  stdout {
      
    }
  }

Не будемо поки що вдаватися до його опису, намагаємося запустити:

docker-compose up

Що ми бачимо?

Контейнер запустився. Можемо перевірити його роботу:

echo '13123123123123123123123213123213' | nc localhost 5046

І бачимо у консолі контейнера відповідь:

Практичне застосування ELK. Налаштовуємо logstash

Але при цьому бачимо також:

logstash_one_channel | [2019-04-29T11:28:59,790][ERROR][logstash.licensechecker.licensereader] Безкоштовна передача license information з license server {:message=>«Elasticsearch Unreachable: [http://elasticsearch:9200/][Manticore ::ResolutionFailure] elasticsearch», …

logstash_one_channel | [2019-04-29T11:28:59,894][INFO ][logstash.pipeline ] Pipeline started successfully {:pipeline_id=>".monitoring-logstash", :thread=>"# »}

logstash_one_channel | [2019-04-29T11:28:59,988][INFO ][logstash.agent ] Pipelines running {:count=>2, :running_pipelines=>[:HABR, :".monitoring-logstash"], :non_running_pipelines=>[ ]}
logstash_one_channel | [2019-04-29T11:29:00,015][ERROR][logstash.inputs.metrics ] X-Pack is installed on Logstash but not on Elasticsearch. Please install X-Pack на Elasticsearch для використання monitoring feature. Інші особливості можуть бути наявними.
logstash_one_channel | [2019-04-29T11:29:00,526][INFO ][logstash.agent ] Successfully started Logstash API endpoint {:port=>9600}
logstash_one_channel | [2019-04-29T11:29:04,478][INFO ][logstash.outputs.elasticsearch] Running health check to see if an Elasticsearch connection is working {:healthcheck_url=>http://elasticsearch:9200/, :path= "/"}
logstash_one_channel | [2019-04-29T11:29:04,487][WARN ][logstash.outputs.elasticsearch] Вважається, що з'ясувати підключення до глухої інстанції, але не буде. {:url=>«еластичний пошук:9200/», :error_type=>LogStash::Outputs::ElasticSearch::HttpClient::Pool::HostUnreachableError, :error=>«Elasticsearch Unreachable: [http://elasticsearch:9200/][Manticore::ResolutionFailure] elasticsearch»}
logstash_one_channel | [2019-04-29T11:29:04,704][INFO ][logstash.licensechecker.licensereader] Running health check to see if an Elasticsearch connection is working {:healthcheck_url=>http://elasticsearch:9200/, : "/"}
logstash_one_channel | [2019-04-29T11:29:04,710][WARN ][logstash.licensechecker.licensereader] Вважається, що вимкнено підключення до глухого ладу ES, але не буде. {:url=>«еластичний пошук:9200/», :error_type=>LogStash::Outputs::ElasticSearch::HttpClient::Pool::HostUnreachableError, :error=>«Elasticsearch Unreachable: [http://elasticsearch:9200/][Manticore::ResolutionFailure] elasticsearch»}

І наш лог весь час повзе вгору.

Тут я виділив зеленим кольором повідомлення про те, що pipeline успішно запустилася, червоним повідомлення про помилку і жовтим повідомлення про спробу зв'язатися з еластичний пошук: 9200.
Відбувається це через те, що в logstash.conf, включеному до складу образу, стоїть перевірка на доступність elasticsearch. Адже logstash передбачає, що працює у складі Elk стеку, а ми його відокремили.

Працювати можна, але не зручно.

Рішенням є відключити цю перевірку через змінну оточення XPACK_MONITORING_ENABLED.

Внесемо зміну до docker-compose.yml і знову запускаємо:

version: '3'

networks:
  elk:

volumes:
  elasticsearch:
    driver: local

services:

  logstash:
    container_name: logstash_one_channel
    image: docker.elastic.co/logstash/logstash:6.3.2
    networks:
      - elk
    environment:
      XPACK_MONITORING_ENABLED: "false"
    ports:
      - 5046:5046
   volumes:
      - ./config/pipelines.yml:/usr/share/logstash/config/pipelines.yml:ro
      - ./config/pipelines:/usr/share/logstash/config/pipelines:ro

Ось тепер все нормально. Контейнер готовий до експериментів.

Можемо знову набрати в сусідній консолі:

echo '13123123123123123123123213123213' | nc localhost 5046

І побачити:

logstash_one_channel | {
logstash_one_channel |         "message" => "13123123123123123123123213123213",
logstash_one_channel |      "@timestamp" => 2019-04-29T11:43:44.582Z,
logstash_one_channel |        "@version" => "1",
logstash_one_channel |     "habra_field" => "Hello Habr",
logstash_one_channel |            "host" => "gateway",
logstash_one_channel |            "port" => 49418
logstash_one_channel | }

Робота в рамках одного каналу

Отже ми запустилися. Тепер власне можна приділити час налаштування безпосередньо logstash. Не будемо поки чіпати файл pipelines.yml, подивимося, що можна отримати, працюючи з одним каналом.

Слід сказати, що загальний принцип роботи з файлом конфігурації каналу добре описаний в офіційному посібнику, ось тут
Якщо хочеться почитати російською, то ми користувалися ось цією статтею(Але синтаксис запитів там старий, треба це враховувати).

Ходімо послідовно від секції Input. Роботу з tcp ми вже бачили. Що ще тут може бути цікавим?

Тестові повідомлення, використовуючи heartbeat

Є така цікава нагода генерувати автоматичні тестові повідомлення.
Для цього в додаткову секцію необхідно включити плагін heartbean.

input {
  heartbeat {
    message => "HeartBeat!"
   }
  } 

Включаємо, починаємо раз на хвилину отримувати

logstash_one_channel | {
logstash_one_channel |      "@timestamp" => 2019-04-29T13:52:04.567Z,
logstash_one_channel |     "habra_field" => "Hello Habr",
logstash_one_channel |         "message" => "HeartBeat!",
logstash_one_channel |        "@version" => "1",
logstash_one_channel |            "host" => "a0667e5c57ec"
logstash_one_channel | }

Бажаємо отримувати частіше, треба додати параметр interval.
Ось так отримуватимемо раз на 10 секунд повідомлення.

input {
  heartbeat {
    message => "HeartBeat!"
    interval => 10
   }
  }

Отримання даних із файлу

Ще вирішили переглянути режим file. Якщо нормально з файлом працює, то, можливо, і агента ніякого не потрібно, ну хоча б для локального використання.

За описом, режим роботи може бути аналогічний tail -f, тобто. читає нові рядки або як опція читає весь файл.

Отже, що ми хочемо отримати:

  1. Ми хочемо отримувати рядки, які дописуються в один файл файл.
  2. Ми хочемо отримувати дані, які записуються в кілька лог файлів, при цьому мати можливість розділити, що звідки отримано.
  3. Ми хочемо перевірити, що при перезапуску logstash він не отримає ці дані повторно.
  4. Ми хочемо перевірити, що якщо logstash відключити, а дані в файли продовжують писатися, то коли ми його запустимо, то ми ці дані отримаємо.

Для проведення експерименту додамо ще один рядок у docker-compose.yml, відкривши директорію, в яку ми кладемо файли.

version: '3'

networks:
  elk:

volumes:
  elasticsearch:
    driver: local

services:

  logstash:
    container_name: logstash_one_channel
    image: docker.elastic.co/logstash/logstash:6.3.2
    networks:
      - elk
    environment:
      XPACK_MONITORING_ENABLED: "false"
    ports:
      - 5046:5046
   volumes:
      - ./config/pipelines.yml:/usr/share/logstash/config/pipelines.yml:ro
      - ./config/pipelines:/usr/share/logstash/config/pipelines:ro
      - ./logs:/usr/share/logstash/input

І змінимо секцію input у habr_pipeline.conf

input {
  file {
    path => "/usr/share/logstash/input/*.log"
   }
  }

Запускаємося:

docker-compose up

Для створення та записування лог файлів будемо користуватися командою:


echo '1' >> logs/number1.log

{
logstash_one_channel |            "host" => "ac2d4e3ef70f",
logstash_one_channel |     "habra_field" => "Hello Habr",
logstash_one_channel |      "@timestamp" => 2019-04-29T14:28:53.876Z,
logstash_one_channel |        "@version" => "1",
logstash_one_channel |         "message" => "1",
logstash_one_channel |            "path" => "/usr/share/logstash/input/number1.log"
logstash_one_channel | }

Ага, працює!

При цьому ми бачимо, що у нас автоматично додалося поле path. Отже, ми зможемо по ньому фільтрувати записи.

Спробуємо ще:

echo '2' >> logs/number1.log

{
logstash_one_channel |            "host" => "ac2d4e3ef70f",
logstash_one_channel |     "habra_field" => "Hello Habr",
logstash_one_channel |      "@timestamp" => 2019-04-29T14:28:59.906Z,
logstash_one_channel |        "@version" => "1",
logstash_one_channel |         "message" => "2",
logstash_one_channel |            "path" => "/usr/share/logstash/input/number1.log"
logstash_one_channel | }

А тепер в інший файл:

 echo '1' >> logs/number2.log

{
logstash_one_channel |            "host" => "ac2d4e3ef70f",
logstash_one_channel |     "habra_field" => "Hello Habr",
logstash_one_channel |      "@timestamp" => 2019-04-29T14:29:26.061Z,
logstash_one_channel |        "@version" => "1",
logstash_one_channel |         "message" => "1",
logstash_one_channel |            "path" => "/usr/share/logstash/input/number2.log"
logstash_one_channel | }

Чудово! Файл підхопився, path вказався правильно, все гаразд.

Зупинимо logstash і запусти заново. Зачекаємо. Тиша. Тобто. Повторно ми цих записів не отримуємо.

А тепер найсміливіший експеримент.

Кладемо logstash і виконуємо:

echo '3' >> logs/number2.log
echo '4' >> logs/number1.log

Знову запускаємо logstash і бачимо:

logstash_one_channel | {
logstash_one_channel |            "host" => "ac2d4e3ef70f",
logstash_one_channel |     "habra_field" => "Hello Habr",
logstash_one_channel |         "message" => "3",
logstash_one_channel |        "@version" => "1",
logstash_one_channel |            "path" => "/usr/share/logstash/input/number2.log",
logstash_one_channel |      "@timestamp" => 2019-04-29T14:48:50.589Z
logstash_one_channel | }
logstash_one_channel | {
logstash_one_channel |            "host" => "ac2d4e3ef70f",
logstash_one_channel |     "habra_field" => "Hello Habr",
logstash_one_channel |         "message" => "4",
logstash_one_channel |        "@version" => "1",
logstash_one_channel |            "path" => "/usr/share/logstash/input/number1.log",
logstash_one_channel |      "@timestamp" => 2019-04-29T14:48:50.856Z
logstash_one_channel | }

Ура! Все підхопилося.

Але треба попередити про наступне. Якщо контейнер з logstash видаляється (docker stop logstash_one_channel && docker rm logstash_one_channel), нічого не підхопиться. Усередині контейнера була збережена позиція файлу, до якої він був рахований. Якщо запускати «з нуля», то він прийматиме лише нові рядки.

Зчитування вже існуючих файлів

Допустимо ми вперше запускаємо logstash, але ми вже маємо логи і ми хотіли б їх обробити.
Якщо ми запустимо logstash з тією секцією input, яку використовували вище, ми нічого не отримаємо. Тільки нові рядки будуть оброблятися logstash.

Для того, щоб підтягнулися рядки з існуючих файлів, слід додати до секції додатковий рядок:

input {
  file {
    start_position => "beginning"
    path => "/usr/share/logstash/input/*.log"
   }
  }

Причому є нюанс, це діє тільки на нові файли, які logstash ще не бачив. Для тих же файлів, що вже потрапляли в поле зору logstash, він вже запам'ятав їх розмір і тепер братиме лише нові записи в них.

Зупинимося у цьому вивчення секції input. Там ще безліч варіантів, але нам для подальших експериментів поки вистачить.

Маршрутизація та перетворення даних

Спробуємо вирішити наступне завдання, припустимо, у нас йдуть повідомлення з одного каналу, частина з них інформаційні, а частина повідомлення про помилки. Вирізняються тегом. Одні INFO, інші ERROR.

Нам потрібно на виході їх поділити. Тобто. Інформаційні повідомлення пишемо до одного каналу, а повідомлення про помилки до іншого.

Для цього від секції input переходимо до filter і output.

За допомогою секції filter ми розберемо вхідне повідомлення, отримавши з нього hash (пари ключ-значення), з яким можна працювати, тобто. розбирати за умовами. А в секції output, відберемо повідомлення та відправимо кожне до свого каналу.

Розбір повідомлень за допомогою grok

Для того, щоб розбирати текстові рядки та отримувати з них набір полів, у секції filter є спеціальний плагін – grok.

Не ставлячи собі за мету дати тут його детальний опис (за цим відсилаю до офіційної документації), наведу свій простий приклад.

Для цього потрібно визначитися з форматом вхідних рядків. У мене вони такі:

1 INFO message1
2 ERROR message2

Тобто. Ідентифікатор на першому місці, потім INFO/ERROR, потім слово без пробілів.
Не складно, але розуміння принципу роботи вистачить.

Отже, у секції filter, у плагіні grok ми повинні визначити патерн для аналізу наших рядків.

Виглядатиме він так:

filter {
  grok {
    match => { "message" => ["%{INT:message_id} %{LOGLEVEL:message_type} %{WORD:message_text}"] }
   }
  } 

По суті, це регулярний вираз. Використовуються готові патерни, такі як INT, LOGLEVEL, WORD. Їх опис, а також інші патерни можна подивитися ось тут

Тепер, проходячи через цей фільтр, наш рядок перетвориться на hash із трьох полів: message_id, message_type, message_text.

Саме вони виводитимуться у секції output.

Маршрутизація повідомлень у секції output за допомогою команди if

У секції output, як ми пам'ятаємо, ми збиралися поділити повідомлення на два потоки. Одні — які iNFO, виводитимемо на консоль, а з помилками, виводитимемо у файл.

Як нам поділити ці повідомлення? Умова завдання вже підказує рішення - адже у нас є вже виділене поле message_type, яке може приймати тільки два значення INFO і ERROR. Саме за ним і зробимо вибір за допомогою оператора if.

if [message_type] == "ERROR" {
        # Здесь выводим в файл
       } else
     {
      # Здесь выводим в stdout
    }

Опис роботи з полями та операторами, можна подивитися ось у цій секції офіційного мануалу.

Тепер про власне сам висновок.

Виведення в консоль, тут все зрозуміло - stdout {}

А ось висновок у файл - згадуємо, що ми це все запускаємо з контейнера і щоб файл, в який ми пишемо результат, був доступний зовні, нам необхідно відкрити цю директорію у docker-compose.yml.

Разом:

Секція output нашого файлу виглядає так:


output {
  if [message_type] == "ERROR" {
    file {
          path => "/usr/share/logstash/output/test.log"
          codec => line { format => "custom format: %{message}"}
         }
    } else
     {stdout {
             }
     }
  }

У docker-compose.yml додаємо ще один том для висновку:

version: '3'

networks:
  elk:

volumes:
  elasticsearch:
    driver: local

services:

  logstash:
    container_name: logstash_one_channel
    image: docker.elastic.co/logstash/logstash:6.3.2
    networks:
      - elk
    environment:
      XPACK_MONITORING_ENABLED: "false"
    ports:
      - 5046:5046
   volumes:
      - ./config/pipelines.yml:/usr/share/logstash/config/pipelines.yml:ro
      - ./config/pipelines:/usr/share/logstash/config/pipelines:ro
      - ./logs:/usr/share/logstash/input
      - ./output:/usr/share/logstash/output

Запускаємо, пробуємо, бачимо поділ на два потоки.

Джерело: habr.com

Додати коментар або відгук