Envie Seus Logs para Loki

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.

Loki

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:

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. É assim que fazíamos interpolação de strings antigamente
  2. Criar a solicitação
  3. 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:

    Java

     

    var 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:

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. O SLF4J é a interface
  2. O Logback é a implementação
  3. Apender Logback dedicado ao SLF4J

Agora, adicionamos um apender Loki específico:

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. O apender Loki
  2. URL do Loki
  3. Quantas etiquetas desejar
  4. Padrão Logback regular

Nosso programa tornou-se muito mais simples:

Java

 

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:

Shell

 

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

Neste ponto, podemos usá-lo em nosso aplicativo de contêiner:

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. Driver de log Loki
  2. URL para enviar
  3. 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:

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