Одна из моих текущих презентаций посвящена Обзорабильности в целом и Распределенному Трассировке в частности, с реализацией на OpenTelemetry. В демонстрации я показываю, как можно наблюдать за трассировками простой распределенной системы, состоящей из API-шлюза Apache APISIX, приложения на Kotlin с Spring Boot, приложения на Python с Flask и приложения на Rust с Axum.
Ранее в этом году я выступал и посещал комнату Обзорабильности на FOSDEM. Одна из презентаций демонстрировала стек Grafana: Mimir для метрик, Tempo для трасс и Loki для логов. Я был приятно удивлен, как можно переходить от одного к другому. Таким образом, я хотел достичь того же в своей демонстрации, но через OpenTelemetry, чтобы избежать привязки к стеку Grafana.
В этом блоге я хотел бы сосредоточиться на логах и Loki.
Основы Loki и Наше Первое Программирование
В своей основе Loki является движком для хранения логов:
Loki представляет собой горизонтально масштабируемую, высокодоступную, многоарендную систему агрегации логов, вдохновленную Prometheus. Он разработан так, чтобы быть очень экономичным и легким в эксплуатации. Он не индексирует содержимое логов, а скорее набор меток для каждого потока логов.
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:
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
}
- Так мы делали интерполяцию строк в старые времена
- Создайте запрос
- Отправьте его
Прототип работает, как видно в Grafana:
Однако, код имеет много ограничений:
- Метка жестко запрограммирована. Вы можете и должны отправить метку одного типа
- Все жестко запрограммировано; ничего не настраивается, например, URL
- Код отправляет один запрос для каждой записи; это крайне неэффективно, так как нет буферизации
- HTTP-клиент синхронный, тем самым блокируя поток, пока ожидается Loki
- Совершенно нет обработки ошибок
- Loki предлагает как сжатие gzip, так и Protobuf; ни один из них не поддерживается моим кодом
-
Наконец, это совершенно не связано с тем, как мы используем логи, например:
Javavar logger = // Получить логгер logger.info("Мое сообщение с параметрами {}, {}", foo, bar);
Regular Logging on Steroids
Для использования вышеупомянутого утверждения, нам нужно выбрать реализацию логирования. Поскольку я более знаком с этим, я буду использовать SLF4J и Logback. Не волнуйтесь, тот же подход работает для Log4J2.
Нам нужно добавить соответствующие зависимости:
<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>
- SLF4J является интерфейсом
- Logback является реализацией
- Appending Logback для SLF4J
Теперь добавим конкретный appender для Loki:
<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>
- Appender Loki
- URL Loki
- Любое количество меток
- Обычный шаблон Logback
Наша программа стала намного проще:
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 предлагает свой плагин. Для его установки:
docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions
На данном этапе мы можем использовать его в нашем приложении контейнера:
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
- Драйвер логирования Loki
- URL для отправки
- Дополнительные метки
Результат следующий. Обратите внимание на стандартные метки.
Заключение
С высоты птичьего полета, Loki не является чем-то необычным: это просто хранилище с RESTful API сверху.
Доступны несколько подходов для использования API. Помимо наивного, мы рассмотрели appender для Java logging framework и Docker. Другие подходы включают сбор файлов логов, например, Promtail, через sidecar Kubernetes. Вы также можете добавить OpenTelemetry Collector между вашим приложением и Loki для выполнения преобразований.
Варианты практически безграничны. Будьте внимательны, чтобы выбрать тот, который лучше всего подходит для вашего контекста.
Для углубления: