Агрегация логов в распределенных системах с 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:
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. Сюда и будут сыпаться логи.
Теперь рассмотрим пример клиента:
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()
}
Компилируем и запускаем:
./client-example proto foo-source foo-job
Если вы собираетесь отправлять логи из нескольких сервисов, убедитесь, что каждый из них имеет уникальное значение source. В противном случае вы получите ошибку:
stream: {source="foo-source", job="foo-job"}
Если все было сделано правильно, в Grafana в разделе Explore станут видны логи.
Само собой разумеется, данная заметка не претендует на то, чтобы быть исчерпывающим руководством по использованию Loki. Более подробную информацию ищите в официальной документации. Полная версия исходников к посту доступна на GitHub.
Метки: Go.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.