Агрегация логов в распределенных системах с Go и Loki

27 февраля 2019

Пару дней назад состоялся релиз Grafana 6.0. Из интересного в данной версии добавили встроенную агрегацию логов. Соответствующее хранилище для логов называется Loki, а агент для записи логов в это хранилище — Promtail. Таким образом, теперь в Grafana можно смотреть не только метрики, но также и логи. Удобно, когда и те, и другие доступны в одном месте. В этой заметке мы научимся писать логи в Promtail / Loki из программ на языке Go.

Примечание: Ранее в этом блоге рассматривались вопросы сбора метрик при помощи Prometheus и Grafana, а также трассировки с помощью Jaeger.

Так получилось, что про Loki мне рассказал Алексей Палажченко за некоторое время до релиза. На тот момент у меня возникли некоторые сложности с использованием официального клиента к Promtail для языка Go. К счастью, протоколы, используемые в Promtail, оказались довольно простыми. Протоколов два. Первый основан на JSON, а второй — на Protobuf. В общем, собственная клиентская библиотека, поддерживающая оба протокола, была написана без особого труда где-то за вечер.

Перед тем, как воспользоваться библиотекой, нам нужно поднять связку из Grafana, Loki и Promtail. Проще всего это сделать при помощи Docker:

mkdir /tmp/loki-test
cd /tmp/loki-test
wget 'https://raw.githubusercontent.com/grafana/loki/'\
'master/production/docker-compose.yaml'
docker-compose pull
docker-compose up
# когда закончили:
# docker-compose down

В браузере открываем http://localhost:3000 и видим там Grafana. Вводим логин admin и пароль admin. Далее жмем Add Data Source → Loki, в поле URL вводим http://loki:3100, сохраняем. В меню слева находим Explore. Сюда и будут сыпаться логи.

Теперь рассмотрим пример клиента:

package main

import (
  "fmt"
  "log"
  "os"
  "time"
  "github.com/afiskon/promtail-client/promtail"
)

func displayUsage() {
  fmt.Fprintf(os.Stderr,
    "Usage: %s proto|json source-name job-name\n", os.Args[0])
  os.Exit(1)
}

func displayInvalidName(arg string) {
  fmt.Fprintf(os.Stderr,
    "Invalid %s: allowed characters are a-zA-Z0-9_-\n", arg)
  os.Exit(1)
}

func nameIsValid(name string) bool {
  for _, c := range name {
        if !((c >= 'a' && c <= 'z') ||
             (c >= 'A' && c <= 'Z') ||
             (c >= '0' && c <= '9') ||
             (c == '-') || (c == '_')) {
            return false
        }
    }
  return true
}

func main() {
  if len(os.Args) < 4 {
    displayUsage()
  }

  format := os.Args[1]
  source_name := os.Args[2]
  job_name := os.Args[3]
  if format != "proto" && format != "json" {
    displayUsage()
  }

  if !nameIsValid(source_name) {
    displayInvalidName("source-name")
  }

  if !nameIsValid(job_name) {
    displayInvalidName("job-name")
  }

  labels := "{source=\""+source_name+"\",job=\""+job_name+"\"}"
  conf := promtail.ClientConfig{
    PushURL:            "http://localhost:3100/api/prom/push",
    Labels:             labels,
    BatchWait:          5 * time.Second,
    BatchEntriesNumber: 10000,
    SendLevel:      promtail.INFO,
    PrintLevel:     promtail.ERROR,
  }

  var (
    loki promtail.Client
    err error
  )

  if format == "proto" {
    loki, err = promtail.NewClientProto(conf)
  } else {
    loki, err = promtail.NewClientJson(conf)
  }

  if err != nil {
    log.Printf("promtail.NewClient: %s\n", err)
    os.Exit(1)
  }

  for i := 1; i < 5; i++ {
    tstamp := time.Now().String()
    loki.Debugf("source = %s time = %s, i = %d\n",
      source_name, tstamp, i)

    // ... аналогично для Infof и Errorf ...

    time.Sleep(1 * time.Second)
  }

  loki.Shutdown()
}

Компилируем и запускаем:

go build
./client-example proto foo-source foo-job

Если вы собираетесь отправлять логи из нескольких сервисов, убедитесь, что каждый из них имеет уникальное значение source. В противном случае вы получите ошибку:

Unexpected HTTP status code: 400, message: entry out of order for
  stream: {source="foo-source", job="foo-job"}

Если все было сделано правильно, в Grafana в разделе Explore станут видны логи.

Само собой разумеется, данная заметка не претендует на то, чтобы быть исчерпывающим руководством по использованию Loki. Более подробную информацию ищите в официальной документации. Полная версия исходников к посту доступна на GitHub. Вопросы и дополнения, как всегда, категорически приветствуются.

Метки: .

Понравился пост? Узнайте, как можно поддержать развитие этого блога.

Также подпишитесь на RSS, Facebook, ВКонтакте, Twitter или Telegram.