Uma das minhas palestras atuais aborda Observabilidade em geral e Rastreamento Distribuído em particular, com uma implementação OpenTelemetry. No demo, mostro como é possível visualizar os rastreamentos de um sistema distribuído simples composto pelo Gateway de API Apache APISIX, um aplicativo Kotlin com Spring Boot, um aplicativo Python com Flask e um aplicativo Rust com Axum.
No início deste ano, ministrei e participei da sala de Observabilidade no FOSDEM. Uma das palestras demonstrou a pilha Grafana: Mimir para métricas, Tempo para rastreamentos e Loki para logs. Fiquei encantado ao ver como se podia passar de um para outro. Assim, quis alcançar o mesmo em meu demo, mas via OpenTelemetry para evitar a vinculação à pilha Grafana.
Neste post de blog, quero focar nos logs e no Loki.
Noções Básicas do Loki e Nosso Primeiro Programa
No cerne dele, Loki é um motor de armazenamento de logs:
Loki é um sistema de agregação de logs escalável horizontalmente, altamente disponível e multi-inquilino inspirado por Prometheus. Foi projetado para ser muito econômico e fácil de operar. Ele não indexa o conteúdo dos logs, mas sim um conjunto de rótulos para cada fluxo de log.
O Loki fornece uma API RESTful para armazenar e ler logs. Vamos enviar um log de um aplicativo Java. O Loki espera a seguinte estrutura de payload:
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
}
- É assim que fazíamos interpolação de strings antigamente
- Criar a solicitação
- Enviar
O protótipo funciona, como visto no Grafana:
No entanto, o código possui muitas limitações:
- A etiqueta é codificada de forma rígida. Você pode e deve enviar uma única etiqueta
- Tudo é codificado de forma rígida; nada é configurável, por exemplo, a URL
- O código envia uma requisição para cada log; é extremamente ineficiente, pois não há bufferização
- O cliente HTTP é síncrono, bloqueando a thread enquanto espera pelo Loki
- Não há tratamento de erros algum
- O Loki oferece compressão gzip e Protobuf; nenhum deles é suportado pelo meu código
-
Finalmente, isso é completamente desvinculado da forma como usamos logs, por exemplo:
Javavar logger = // Obtenha o logger logger.info("Meu mensagem com parâmetros {}, {}", foo, bar);
Registro Regular com Steróides
Para usar a afirmação acima, precisamos escolher uma implementação de registro. Como estou mais familiarizado com isso, vou usar o SLF4J e o Logback. Não se preocupe; a mesma abordagem funciona para o Log4J2.
Precisamos adicionar as dependências relevantes:
<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>
- O SLF4J é a interface
- O Logback é a implementação
- Apender Logback dedicado ao SLF4J
Agora, adicionamos um apender Loki específico:
<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>
- O apender Loki
- URL do Loki
- Quantas etiquetas desejar
- Padrão Logback regular
Nosso programa tornou-se muito mais simples:
var who = //...
var logger = LoggerFactory.getLogger(Main.class.toString());
logger.info("Hello from {}!", who);
Grafana exibe o seguinte:
Registro em Docker
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.
No entanto, existem outras opções além de salvar em um arquivo local, por exemplo, syslog
, Google Cloud, Splunk, etc. Para escolher uma opção diferente, configura-se um driver de log. Pode-se configurar o driver no nível geral do Docker ou por contêiner.
O Loki oferece seu próprio plugin. Para instalá-lo:
docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions
Neste ponto, podemos usá-lo em nosso aplicativo de contêiner:
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
- Driver de log Loki
- URL para enviar
- Etiquetas adicionais
O resultado é o seguinte. Observe as etiquetas padrão.
Conclusão
Do ponto de vista de um pássaro, o Loki não é nada extraordinário: é um simples mecanismo de armazenamento com uma API RESTful em cima.
Várias abordagens estão disponíveis para usar a API. Além da ingênua, vimos um apender de framework de log Java e Docker. Outras abordagens incluem raspagem dos arquivos de log, por exemplo, Promtail, via sidecar Kubernetes. Você também poderia adicionar um OpenTelemetry Collector entre seu aplicativo e o Loki para realizar transformações.
As opções são praticamente ilimitadas. Tenha cuidado para escolher a que melhor se adapta ao seu contexto.
Para ir mais longe: