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

EnglishRussianUkrainian