Envie seus logs para o Loki

Um dos meus atuais discursos aborda a Observabilidade em geral e o Rastreamento Distribuído em particular, com uma implementação OpenTelemetry. Na demonstração, 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, falei 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 mudar de um para o outro. Assim, quis alcançar o mesmo em minha demonstração, mas via OpenTelemetry para evitar a vinculação à pilha Grafana.

Neste post no blog, quero focar nos logs e no Loki.

Noções Básicas do Loki e Nosso Primeiro Programa

Em seu núcleo, Loki é um mecanismo de armazenamento de logs:

Loki é um sistema de agregação de logs escalável horizontalmente, altamente disponível e multi-inquilino, inspirado por Prometheus. Ele é 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. Foi assim que fazíamos interpolação de strings antigamente
  2. Criar a requisição
  3. Enviar

O protótipo funciona, como visto no Grafana:

No entanto, o código possui muitas limitações:

  • A etiqueta está codificada de forma rígida. Você pode e deve enviar uma única etiqueta
  • Tudo está 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 é sincrônico, bloqueando a thread enquanto espera o Loki
  • Não há tratamento de erros
  • O Loki oferece compressão gzip e Protobuf; nenhum deles é suportado pelo meu código
  • Por fim, isso é completamente irrelevante em relação a 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 declaraçã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 do 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. Tantos rótulos quanto desejados
  4. Padrão regular do Logback

Nosso programa se tornou muito mais simples:

Java

 

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

O 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, define-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. Rótulos adicionais

O resultado é o seguinte. Observe os rótulos 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 do 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