Отправьте Ваши журналы в Loki

Одна из моих текущих презентаций посвящена Обзорабильности в целом и Распределенному Трассировке в частности, с реализацией на OpenTelemetry. В демонстрации я показываю, как можно наблюдать за трассировками простой распределенной системы, состоящей из API-шлюза Apache APISIX, приложения на Kotlin с Spring Boot, приложения на Python с Flask и приложения на Rust с Axum.

Ранее в этом году я выступал и посещал комнату Обзорабильности на FOSDEM. Одна из презентаций демонстрировала стек Grafana: Mimir для метрик, Tempo для трасс и Loki для логов. Я был приятно удивлен, как можно переходить от одного к другому. Таким образом, я хотел достичь того же в своей демонстрации, но через OpenTelemetry, чтобы избежать привязки к стеку Grafana.

В этом блоге я хотел бы сосредоточиться на логах и Loki.

Основы Loki и Наше Первое Программирование

В своей основе Loki является движком для хранения логов:

Loki представляет собой горизонтально масштабируемую, высокодоступную, многоарендную систему агрегации логов, вдохновленную Prometheus. Он разработан так, чтобы быть очень экономичным и легким в эксплуатации. Он не индексирует содержимое логов, а скорее набор меток для каждого потока логов.

Loki

Loki предоставляет RESTful API для хранения и чтения логов. Давайте отправим логи из Java-приложения. Loki ожидает следующую структуру данных:

I’ll use Java, but you can achieve the same result with a different stack. The most straightforward code is the following:

Java

 

public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
    var template = "'{' \"streams\": ['{' \"stream\": '{' \"app\": \"{0}\" '}', \"values\": [[ \"{1}\", \"{2}\" ]]'}']'}'"; //1
    var now = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant();
    var nowInEpochNanos = NANOSECONDS.convert(now.getEpochSecond(), SECONDS) + now.getNano();
    var payload = MessageFormat.format(template, "demo", String.valueOf(nowInEpochNanos), "Hello from Java App");           //1
    var request = HttpRequest.newBuilder()                                                                                  //2
            .uri(new URI("http://localhost:3100/loki/api/v1/push"))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(payload))
            .build();
    HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());                                         //3
}

  1. Так мы делали интерполяцию строк в старые времена
  2. Создайте запрос
  3. Отправьте его

Прототип работает, как видно в Grafana:

Однако, код имеет много ограничений:

  • Метка жестко запрограммирована. Вы можете и должны отправить метку одного типа
  • Все жестко запрограммировано; ничего не настраивается, например, URL
  • Код отправляет один запрос для каждой записи; это крайне неэффективно, так как нет буферизации
  • HTTP-клиент синхронный, тем самым блокируя поток, пока ожидается Loki
  • Совершенно нет обработки ошибок
  • Loki предлагает как сжатие gzip, так и Protobuf; ни один из них не поддерживается моим кодом
  • Наконец, это совершенно не связано с тем, как мы используем логи, например:

    Java

     

    var logger = // Получить логгер
    logger.info("Мое сообщение с параметрами {}, {}", foo, bar);

Regular Logging on Steroids

Для использования вышеупомянутого утверждения, нам нужно выбрать реализацию логирования. Поскольку я более знаком с этим, я буду использовать SLF4J и Logback. Не волнуйтесь, тот же подход работает для Log4J2.

Нам нужно добавить соответствующие зависимости:

XML

 

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>             <!--1-->
    <version>2.0.7</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>       <!--2-->
    <version>1.4.8</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>com.github.loki4j</groupId>
    <artifactId>loki-logback-appender</artifactId> <!--3-->
    <version>1.4.0</version>
    <scope>runtime</scope>
</dependency>

  1. SLF4J является интерфейсом
  2. Logback является реализацией
  3. Appending Logback для SLF4J

Теперь добавим конкретный appender для Loki:

XML

 

<appender name="LOKI" class="com.github.loki4j.logback.Loki4jAppender">                   <!--1-->
    <http>
        <url>http://localhost:3100/loki/api/v1/push</url>                                 <!--2-->
    </http>
    <format>
        <label>
            <pattern>app=demo,host=${HOSTNAME},level=%level</pattern>                     <!--3-->
        </label>
        <message>
            <pattern>l=%level h=${HOSTNAME} c=%logger{20} t=%thread | %msg %ex</pattern>  <!--4-->
        </message>
        <sortByTime>true</sortByTime>
    </format>
</appender>
<root level="DEBUG">
    <appender-ref ref="STDOUT" />
</root>

  1. Appender Loki
  2. URL Loki
  3. Любое количество меток
  4. Обычный шаблон Logback

Наша программа стала намного проще:

Java

 

var who = //...
var logger = LoggerFactory.getLogger(Main.class.toString());
logger.info("Hello from {}!", who);

Grafana отображает следующее:

Docker Logging

I’m running most of my demos on Docker Compose, so I’ll mention the Docker logging trick. When a container writes on the standard out, Docker saves it to a local file. The docker logs  command can access the file content.

Однако доступны и другие варианты, кроме сохранения в локальный файл, например, syslog, Google Cloud, Splunk и т.д. Чтобы выбрать другой вариант, следует установить драйвер логирования. Его можно настроить как для всего Docker, так и для каждого контейнера.

Loki предлагает свой плагин. Для его установки:

Shell

 

docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions

На данном этапе мы можем использовать его в нашем приложении контейнера:

YAML

 

services:
  app:
    build: .
    logging:
      driver: loki                                                    #1
      options:
        loki-url: http://localhost:3100/loki/api/v1/push              #2
        loki-external-labels: container_name={{.Name}},app=demo       #3

  1. Драйвер логирования Loki
  2. URL для отправки
  3. Дополнительные метки

Результат следующий. Обратите внимание на стандартные метки.

Заключение

С высоты птичьего полета, Loki не является чем-то необычным: это просто хранилище с RESTful API сверху.

Доступны несколько подходов для использования API. Помимо наивного, мы рассмотрели appender для Java logging framework и Docker. Другие подходы включают сбор файлов логов, например, Promtail, через sidecar Kubernetes. Вы также можете добавить OpenTelemetry Collector между вашим приложением и Loki для выполнения преобразований.

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

Для углубления:

Source:
https://dzone.com/articles/send-your-logs-to-loki