Consumiento Loki en PHP, una solución personalizada

Consumiento Loki en PHP, una solución personalizada

Si han estado siguiendo algunos de mis artículos, verán que estuve haciendo migraciones de Elasticsearch hacia Loki para almacenamiento de logs y como es de costumbre, nuevamente me enfrenté con algo que esperaba solucionar rápido, pero no fue así.

Los pongo en contexto

Loki, desarrollado por Grafana Labs, ha ganado popularidad como una solución de almacenamiento de logs ligera, especialmente diseñada para ser utilizada en ambientes de contenedores como Kubernetes. Mientras que Elasticsearch ha sido un jugador dominante en el espacio de búsqueda y análisis de logs por bastante tiempo, la simplicidad y eficiencia de Loki lo hicieron una opción atractiva para mi proyecto.

Al principio, estaba utilizando Elasticsearch para almacenar y luego leer logs directamente desde una aplicación basada en PHP y Laravel. Pero con la transición a Loki, me encontré con un obstáculo: no pude encontrar una biblioteca en PHP que me permitiera interactuar fácilmente con Loki.

Piensa rápido y crea una solución

En lugar de esperar o buscar en profundidad una solución preexistente, decidí leer un poco y resolverlo de la forma en que otro dev con algo de experiencia lo habría hecho y así fue como escribí mi propia solución. Utilizando la API HTTP de Loki, escribí una clase en PHP que, con la ayuda de Guzzle (un popular cliente HTTP para PHP), me permitiría solicitar y procesar los logs que necesitaba.

Una de las características más destacadas de esta solución es su flexibilidad. El campo de consulta, o query, puede adaptarse y extenderse según las necesidades específicas. En mi caso, estaba buscando todos los logs relacionados con un objeto en particular, algo similar a hacer un seguimiento con un trace_id.

La solucion final

Les comparto lo que finalmente fue el código que escribí para resolver el problema, a decir verdad fue bastante sencillo y no necesitaba de ninguna biblioteca, todo lo tuve siempre a la mano, si tuve que leer un poco y aprender sobre las consultas en Loki, que de paso, me sirvió mucho explorar un poco más Grafana para entender como lo hacen y que necesitaba.

<?php
/**
 * LokiClient.php
 *
 * PHP version 7
 *
 * @category Helpers
 * @license  http://www.gnu.org/copyleft/gpl.html GNU General Public License
 * @since    1.0.0
 */
namespace App\Http\Helpers;

use Illuminate\Support\Facades\Http;
use GuzzleHttp\Psr7;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

/**
 * LokiClient class
 *
 * @category Helpers
 * @license  http://www.gnu.org/copyleft/gpl.html GNU General Public License
 * @since    1.0.0
 */
class LokiClient {
    protected $server = 'default';
    protected $client;

    public function __construct($config = null)
    {
        $lokiProtocol = env('LOKI_PROTOCOL');
        $lokiHost = env('LOKI_HOST');
        $lokiPort = env('LOKI_PORT');

        $this->server = "$lokiProtocol://$lokiHost:$lokiPort";
        $this->client = new Client();
    }

    /**
     * Busca logs de un objeto específico en Loki.
     * 
     * @param int $gigId ID del objeto
     * @param int $start Tiempo de inicio en milisegundos
     * @param int $end Tiempo final en milisegundos
     * @return Illuminate\Http\JsonResponse Respuesta JSON con los logs o el error
     */
    public function searchObject(int $objectId, int $start, int $end) {
        $urlRequest = "$this->server/loki/api/v1/query_range";

        try {
            $request = $this->client->get($urlRequest, [
                'query' => [
                    'query' => '{level=~"error|info|warn"} | json | meta_object_id=`'.$objectId.'`',
                    'limit' => 500,
                    'start' => $start,
                    'end' => $end
                ]
            ]);

            $response = response()->json(json_decode($request->getBody()->getContents()));
        } catch(RequestException $e) {
            $message = $e->getResponse()->getBody()->getContents();
            $response = response()->json([
                'error' => $message,
            ]);
        }

        return $response;
    }
}

Este archivo es parte de un proyecto más grande en que se utiliza Laravel, pero podrán darse una idea de la estructura y los paquetes que estoy utilizando.

Algo importante a considerar es que el query que he escrito específicamente funciona para la estructura de logs que estoy almacenando, donde object_id es una propiedad de meta y level como fue explicado en este otro artículo, puede representar diferentes niveles de logs, al final el objeto almacenado en Loki es algo similar a esto:

{
   level: 'info',
   message: 'Se agrego un nuevo objeto',
   meta: {
      object_id: 123,
      ...extra_params,
   }
}

Espero que esto les sirva y puedan crear algo más complejo, porque como siempre digo, lo que esta expuesto aquí, siempre se puede mejorar.

Happy coding! :D


Photo by Shane Young on Unsplash

Jack Fiallos

Jack Fiallos

Te gustó este artículo?