Uno de mis actuales charlas se centra en la Observabilidad en general y el Rastreo Distribuido en particular, con una implementación de OpenTelemetry. En la demo, muestro cómo puedes ver las trazas de un sistema distribuido simple que consiste en el API Gateway de Apache APISIX, una aplicación en Kotlin con Spring Boot, una aplicación en Python con Flask y una aplicación en Rust con Axum.
A principios de este año, hablé y asistí a la sala de Observabilidad en FOSDEM. Una de las charlas mostró la pila de Grafana: Mimir para métricas, Tempo para trazas y Loki para logs. Me sorprendió gratamente cómo se podía pasar de uno a otro. Por lo tanto, quería lograr lo mismo en mi demo pero a través de OpenTelemetry para evitar la dependencia de la pila de Grafana.
En esta entrada de blog, quiero centrarme en los logs y Loki.
Conceptos Básicos de Loki y Nuestro Primer Programa
En su núcleo, Loki es un motor de almacenamiento de logs:
Loki es un sistema de agregación de logs escalable horizontalmente, altamente disponible y multiinquilino inspirado en Prometheus. Está diseñado para ser muy rentable y fácil de operar. No indexa el contenido de los logs, sino un conjunto de etiquetas para cada flujo de logs.
Loki ofrece una API RESTful para almacenar y leer logs. Vamos a enviar un log desde una aplicación Java. Loki espera la siguiente estructura de carga útil:
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
}
- Así es como hacíamos interpolación de cadenas en los viejos tiempos
- Crear la solicitud
- Enviarla
El prototipo funciona, como se ve en Grafana:
Sin embargo, el código tiene muchas limitaciones:
- La etiqueta está codificada de forma rígida. Puedes y debes enviar una única etiqueta
- Todo está codificado de forma rígida; nada es configurable, por ejemplo, la URL
- El código envía una solicitud por cada registro; es enormemente ineficiente ya que no hay almacenamiento en buffer
- El cliente HTTP es sincrónico, bloqueando el hilo mientras espera a Loki
- No hay manejo de errores en absoluto
- Loki ofrece compresión gzip y Protobuf; ninguno está soportado con mi código
-
Finalmente, no tiene ninguna relación con cómo utilizamos los registros, por ejemplo:
Javavar logger = // Obtener logger logger.info("Mi mensaje con parámetros {}, {}", foo, bar);
Registro Regular Al Estilo de Súper Héroes
Para utilizar la declaración anterior, necesitamos elegir una implementación de registro. Como soy más familiarizado con él, usaré SLF4J y Logback. No se preocupen; el mismo enfoque funciona para Log4J2.
Necesitamos añadir las dependencias 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>
- SLF4J es la interfaz
- Logback es la implementación
- Apendice de Logback dedicado a SLF4J
Ahora, agregamos un apéndice 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>
- El apéndice Loki
- URL de Loki
- Tantas etiquetas como se deseen
- Patrón regular de Logback
Nuestro programa se ha vuelto mucho más sencillo:
var who = //...
var logger = LoggerFactory.getLogger(Main.class.toString());
logger.info("Hello from {}!", who);
Grafana muestra lo siguiente:
Registro de 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.
Sin embargo, hay otras opciones además de guardar en un archivo local, por ejemplo, syslog
, Google Cloud, Splunk, etc. Para elegir una opción diferente, se establece un controlador de registro. Se puede configurar el controlador a nivel general de Docker o por contenedor.
Loki ofrece su propio plugin. Para instalarlo:
docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions
En este punto, podemos usarlo en nuestra aplicación de contenedor:
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
- Controlador de registro de Loki
- URL a la que enviar
- Etiquetas adicionales
El resultado es el siguiente. Tenga en cuenta las etiquetas predeterminadas.
Conclusión
Desde una perspectiva general, Loki no es nada extraordinario: es un motor de almacenamiento plano con una API RESTful en la parte superior.
Hay varios enfoques disponibles para usar la API. Más allá del enfoque ingenuo, hemos visto un apéndice de framework de registro Java y Docker. Otros enfoques incluyen raspar los archivos de registro, por ejemplo, Promtail, a través de un sidecar de Kubernetes. También podría agregar un OpenTelemetry Collector entre su aplicación y Loki para realizar transformaciones.
Las opciones son prácticamente ilimitadas. Tenga cuidado al elegir la que mejor se adapte a su contexto.
Para avanzar más: