DEV Community

Cover image for Loki na prática - enviando logs via API HTTP a partir das mini apps Python e PHP
Rafael Dutra for apsis-cc

Posted on

Loki na prática - enviando logs via API HTTP a partir das mini apps Python e PHP

1. Retomando: métricas prontas, falta o segundo pilar

As duas mini aplicações desta série — a de Python (artigo 2) e a de PHP (artigo 3) — já expõem métricas consumidas pelo Prometheus. Métricas respondem "quantos pedidos falharam nos últimos 5 minutos", mas não respondem "por que o pedido 4821 falhou especificamente". Essa é a pergunta que logs respondem, e este artigo cobre como enviá-los para o Loki diretamente das mini apps.

2. Por que a indexação do Loki é diferente

Ferramentas de log tradicionais, como o Elasticsearch, indexam o conteúdo completo de cada linha — todo termo de texto vira pesquisável, o que permite buscas ricas, mas exige estruturas de índice caras em disco e CPU, que crescem proporcionalmente ao volume de log.

O Loki toma uma decisão deliberadamente diferente, inspirada diretamente no modelo do Prometheus: indexa só um conjunto pequeno de labels — como app="python-app", env="production" — e trata o conteúdo da linha de log como texto opaco, comprimido e armazenado em blocos (chunks), sem índice próprio. Quando uma consulta pede um filtro de texto (|= "erro", por exemplo), o Loki varre os chunks relevantes (já filtrados pelos labels) no momento da consulta, em vez de consultar um índice pré-construído.

O resultado prático dessa troca:

  • Armazenamento muito mais barato — sem índice invertido de texto completo, o custo de disco cresce bem mais devagar com o volume de logs.
  • Buscas por label são muito rápidas — "todos os logs do serviço X, no ambiente Y" é uma operação barata, porque os labels são de fato indexados.
  • Buscas por texto livre são mais lentas conforme o volume cresce — porque envolvem varredura, não um lookup de índice. Na prática, isso empurra o usuário a sempre filtrar primeiro por labels (que reduzem drasticamente o volume a varrer) antes de aplicar um filtro de texto.
  • Cardinalidade de labels importa tanto quanto no Prometheus — usar um label com alta cardinalidade (como um user_id único por requisição) quebra a mesma premissa de eficiência nos dois sistemas, e é um erro comum de quem vem de ferramentas que indexam tudo.

3. A API HTTP de push do Loki

Diferente do Prometheus (que faz pull), o Loki recebe logs por push: a aplicação envia ativamente cada lote de linhas para um endpoint HTTP, POST /loki/api/v1/push. O corpo da requisição é um JSON com o seguinte formato:

{
  "streams": [
    {
      "stream": {
        "app": "python-app",
        "level": "info"
      },
      "values": [
        ["1735689600000000000", "requisicao recebida em /"]
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Pontos importantes desse formato:

  • stream carrega os labels — o que o Loki de fato indexa. Poucos e de baixa cardinalidade, como visto acima.
  • values é uma lista de [timestamp, linha], onde o timestamp é uma string em nanosegundos desde epoch (não segundos, nem milissegundos — um detalhe fácil de errar).
  • É possível enviar múltiplas linhas do mesmo stream de uma vez, o que é mais eficiente do que uma requisição HTTP por linha de log.

4. Enviando logs a partir da mini app Python

Usando só a biblioteca padrão (urllib, sem instalar um client HTTP externo), uma função de log que envia diretamente para o Loki:

# logger.py
import json
import time
import urllib.request

LOKI_URL = "http://loki:3100/loki/api/v1/push"


def log_to_loki(message, level="info", app="python-app"):
    timestamp_ns = str(time.time_ns())
    payload = {
        "streams": [
            {
                "stream": {"app": app, "level": level},
                "values": [[timestamp_ns, message]],
            }
        ]
    }
    req = urllib.request.Request(
        LOKI_URL,
        data=json.dumps(payload).encode("utf-8"),
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    try:
        urllib.request.urlopen(req, timeout=2)
    except Exception as exc:
        # nunca deixar uma falha de log derrubar a aplicacao
        print(f"falha ao enviar log para o Loki: {exc}")
Enter fullscreen mode Exit fullscreen mode

Integrando no app.py do artigo 2, chamando log_to_loki a cada requisição de negócio:

# dentro do do_GET, no branch que nao eh /metrics
log_to_loki(f"requisicao recebida em {self.path}")
Enter fullscreen mode Exit fullscreen mode

5. Enviando logs a partir da mini app PHP

O mesmo endpoint, chamado a partir do PHP puro com file_get_contents e um stream context (sem precisar de cURL nem bibliotecas externas):

<?php
// logger.php

function logToLoki($message, $level = 'info', $app = 'php-app') {
    // microtime(true) da segundos com fracao decimal; convertendo para
    // nanossegundos desde epoch, o formato que o Loki espera
    $timestampNs = sprintf('%.0f', microtime(true) * 1e9);

    $payload = json_encode([
        'streams' => [[
            'stream' => ['app' => $app, 'level' => $level],
            'values' => [[$timestampNs, $message]],
        ]],
    ]);

    $context = stream_context_create([
        'http' => [
            'method' => 'POST',
            'header' => "Content-Type: application/json\r\n",
            'content' => $payload,
            'timeout' => 2,
            'ignore_errors' => true,
        ],
    ]);

    @file_get_contents('http://loki:3100/loki/api/v1/push', false, $context);
}
Enter fullscreen mode Exit fullscreen mode

Integrando no app.php do artigo 3, logo após processar um pedido:

require __DIR__ . '/logger.php';
logToLoki("pedido processado em {$state['requests_total']} ms de fila");
Enter fullscreen mode Exit fullscreen mode

6. Consultando os logs no Grafana com LogQL

Com o datasource do Loki já configurado no Grafana desde o artigo 1, em Explore, selecionando o datasource Loki, uma consulta LogQL básica:

{app="python-app"}
Enter fullscreen mode Exit fullscreen mode

Isso retorna todas as linhas de log com o label app="python-app" — o equivalente a um grep pré-filtrado pelos labels indexados. Para refinar com um filtro de texto:

{app="python-app"} |= "erro"

{app="php-app", level="info"} != "healthcheck"
Enter fullscreen mode Exit fullscreen mode

|= filtra linhas que contêm o texto; != filtra linhas que não contêm. Esses filtros de texto rodam sobre o conteúdo já restrito pelos labels — daí a importância de sempre começar a query pelos labels certos, como discutido na seção 2.

7. Conclusão e próximos passos

Vimos por que o Loki indexa só labels e trata o conteúdo da linha como texto não indexado — uma troca deliberada de riqueza de busca por custo —, como enviar logs diretamente via API HTTP sem bibliotecas externas em Python e PHP, e como consultar esses logs com LogQL no Grafana. Com métricas e logs das duas mini apps já fluindo para a stack, falta o terceiro pilar: o próximo artigo introduz o OpenTelemetry e instrumenta as mesmas aplicações para gerar traces.


Imagem de capa: Logo oficial do Prometheus — repositório prometheus/prometheus, licença Apache 2.0 (convertido para PNG via wsrv.nl)

Referências:

  1. Grafana Loki — HTTP API (push)
  2. Grafana Loki — LogQL
  3. Grafana Loki — Best Practices

Top comments (0)