Практычнае прымяненне 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. Вы можаце наладзіць X-Pack на Elasticsearch, каб выкарыстоўваць для monitoring feature. Other features may be available.
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 check check to see if Elasticsearch connection is working {:healthcheck_url=>http://elasticsearch:9200/, :path= "/"}
logstash_one_channel | [2019-04-29T11:29:04,487][WARN ][logstash.outputs.elasticsearch] Attempted to resurrect connection to dead in instance, but got an error. {: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 check check to see if Elasticsearch connection is working {:healthcheck_url=>http://elasticsearch:9200/, : "/"}
logstash_one_channel | [2019-04-29T11:29:04,710][WARN ][logstash.licensechecker.licensereader] Attempted to resurrect connection to dead in instance, but got an error. {: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

Ёсць такая цікавая магчымасць генераваць аўтаматычныя тэставыя паведамленні.
Для гэтага ў input секцыю трэба ўключыць убудову 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 секцыю дадатковы радок:

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

Дадаць каментар