← На главную

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

Пару дней назад состоялся релиз 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.