# Loki

Las veintidós medidas que son eventos y no números, y el horizonte de antigüedad que evita que se rechace un envío.

Source: https://docs.jmrp.io/ghchronicle/es/sinks/loki/

El destino Loki envía a Grafana Loki, como líneas de log, la actividad de GitHub que es un evento y no un número (una estrella, una release, una ejecución fallida).

```yaml
sinks:
  loki:
    url: http://loki:3100/loki/api/v1/push
    tenant_id: ""
    labels:
      job: ghchronicle
    max_age: 1h
    batch: 1000
```

## Medidas y eventos

Parte de lo que informa GitHub es una medida y parte es un evento. "El
repositorio tiene 148 estrellas" es una medida. "Alguien le dio una estrella a
las 03:03, se publicó esta release, aquel workflow falló en main, se abrió esta
alerta" son eventos: cada uno ocurrió una vez, en un momento conocido, y lo que
uno quiere después es leerlos en orden y buscarlos, no promediarlos.

**Veintidós medidas tienen una representación de evento**: estrellas en ambas
direcciones, forks, releases, versiones de paquete publicadas, pull requests,
revisiones, hilos de revisión, issues, commits, ejecuciones de workflows, logs
de jobs, actividad del repositorio, alertas de Dependabot, análisis de code
scanning, el feed de eventos, las notificaciones, las discusiones, las entregas
de webhook, los despliegues, las versiones de ruleset y las contribuciones
externas. Todo lo demás es un gauge disfrazado y no se envía.

Una medida sin representación se descarta en silencio, lo cual está bien para un
gauge y mal para un evento que nadie ha llegado a escribir: los despliegues y los
hilos de revisión fueron eventos fechados sin línea de log durante meses y nada
lo dijo. Por eso ahora toda medida fechada tiene que aparecer en una de las dos
tablas de `internal/sink/loki.go`, la de representaciones o la de rechazos, y
cada rechazo lleva el motivo por el que no es una línea de log. Una prueba falla
ante una medida fechada que no aparezca en ninguna de las dos.

En el registro de la pasada ya no se descarta en silencio. Este destino escribe
una fracción de lo que se le ofrece, así que la mayoría de sus líneas llevan
[`filtered`](https://docs.jmrp.io/ghchronicle/es/sinks/#qué-cuenta-la-línea-de-escritura) y
`points=0`, que es lo que guardó y no lo que se le dio. Una entrada más vieja
que `max_age`, o demasiado atrasada respecto a la más reciente de su propio
flujo para que Loki la acepte, también se cuenta ahí y se informa en una línea
de entradas descartadas.

## El formato de línea

Cada línea se lee primero como una frase y lleva detrás cada etiqueta y cada
campo en logfmt, así que la misma línea es greppable en una terminal y
consultable en Grafana sin mantener dos copias de los datos.

```text
someone starred acme/telemetry full_name="acme/telemetry" user="someone" starred=1
```

Dos representaciones cambiaron en 2.6.0, y una de ellas otra vez en 2.6.1. Una
línea que ya está en Loki conserva el texto con el que se envió:

- Una contribución externa es una línea cuando el elemento se cierra, en el
  momento en que se cerró, y dice qué le pasó al elemento y no quién lo hizo,
  porque la fila no dice quién lo fusionó o lo cerró: una pull request dice
  `USER's pull request OWNER/REPO#N was merged` o
  `USER's pull request OWNER/REPO#N was closed without merging`, y una issue
  `USER's issue OWNER/REPO#N was closed`. Una fila cerrada cuyo estado y cuya
  marca de fusión no coinciden dice `USER's contribution OWNER/REPO#N`. Un
  elemento todavía abierto no es ninguna línea desde 2.6.1: su fila se sella
  al principio de cada día en que se ve abierto, lo que es una lectura y no
  algo que ocurrió, y abrirlo ya es una línea del feed de eventos, con
  `action="opened"`. 2.6.0 enviaba esa fila como `... is open`, y solo el día
  en que una pasada de `outbound` escribía en la primera hora del día UTC.
  Antes todas las líneas decían `USER merged OWNER/REPO#N`, issues abiertas
  incluidas.
- Una release dice `published release TAG of OWNER/REPO`, o
  `published prerelease TAG of OWNER/REPO`, en el momento en que se publicó.
  Antes decía `release TAG of OWNER/REPO, N downloads`, así que una consulta
  que filtre por `downloads` solo encuentra las líneas antiguas.

`batch` es cuántas entradas van en un envío, 1000 salvo que se baje.

La etiqueta de stream es `kind`, que es por lo que se filtra primero.

```text
{job="ghchronicle", kind="workflow_run"} |= "failure"
{job="ghchronicle", kind="job_log"}
```

> **Deja pocas etiquetas más**
>
> Loki indexa las etiquetas, y una etiqueta de cardinalidad alta cuesta mucho
> más que una línea ancha. `labels` en la configuración es para las fijas que
> identifican a este colector, no para nada que varíe por punto.

## `max_age`, y por qué la razón no es la obvia

Loki rechaza un envío entero cuando una entrada es anterior a
`reject_old_samples_max_age`, una semana por omisión, y la mitad de lo que
produce este colector es más viejo que eso a propósito: una estrella de 2020, un
pull request de 2024.

Pero el límite que de verdad muerde es el otro. Loki también rechaza una entrada
que esté más atrasada que su ventana de desorden respecto a la entrada más nueva
que ya hay en ese stream, que es la mitad del `max_chunk_age` del ingester, una
hora por omisión.

**Medido contra un Loki 3 real**: una vez que el stream tenía una entrada de las
19:14, una de las 00:35 del mismo día volvió como "entry too far behind". Contra
Loki 3.7.7 con sus límites por omisión, un stream con una entrada de hace cinco
minutos aceptó una de hace 55 minutos y rechazó una de hace 75.

Así que el horizonte se aplica de dos formas:

1. contra el reloj de pared,
2. contra la entrada más nueva que se ha enviado antes a ese stream.

Dentro de un mismo envío nada va atrasado, porque el destino envía cada stream
de la más vieja a la más nueva y Loki juzga cada entrada contra la más nueva
anterior a ella: un envío con entradas de hace 23 horas, 12 horas y un minuto a
un stream vacío se aceptó entero.

Lo que cae fuera se deja fuera y se cuenta, a nivel de depuración, en vez de
costar el envío entero.

`max_age` vale una hora por omisión, que es la ventana por defecto de Loki.
Súbelo solo si has subido a juego el `max_chunk_age` de Loki, ya que la ventana
es la mitad.

Una release es el evento que muestra lo que cuesta el horizonte, y uno de los
dos streams que miran más atrás. Su línea se genera a partir de
`gh_release_published`, en el momento en que se publicó la release. Antes salía
de `gh_release`, que se sella en la pasada porque sus descargas se mueven, así
que cada pasada de repositorios volvía a enviar todas las releases. Medido con
2.5.1 en producción, durante 30,9 horas y 27 pasadas de `repo`, fueron 4.313 de
las 10.467 líneas que envió el destino, un 41 por ciento, para las 2 releases
publicadas en esas horas. Fechada en la publicación, una release la ve primero la pasada de
`repo` siguiente, así que una publicada justo después de que una pasada leyera
su repositorio tiene una cadencia entera cuando escribe la siguiente pasada, más
lo que esa pasada se retrase: un tick que perdió, las familias más lentas que
corrieron antes, un reinicio. Con `repo` y `max_age` en su hora por omisión,
`max_age` por sí solo dejaba fuera esa release, y cada pasada posterior solo la
veía más vieja.

Así que el stream de releases mira atrás la cadencia de `repo` más `max_age`:
dos horas por omisión, siete con `repo: 6h`. Una contribución externa es el
otro stream que lo hace, por la misma razón: su línea se fecha cuando se cerró
el elemento, y la primera pasada que lo ve es la de `outbound` siguiente, así
que el stream mira atrás la cadencia de `outbound` más `max_age`, dos horas por
omisión. Antes de 2.6.1 tenía solo `max_age`, y un cierre se enviaba solo si la
pasada siguiente escribía dentro de la hora, cosa que una pasada horaria no
hace con un elemento cerrado en los segundos posteriores a que la anterior
leyera las búsquedas, ni, cuando se retrasa, con uno cerrado en los minutos que
lleva de retraso.

Cada mirada atrás tiene un tope de seis días, un día menos que la semana que
permite `reject_old_samples_max_age`, y nunca acorta `max_age`: uno fijado por
encima de seis días es el horizonte de estos streams igual que el de cualquier
otro. Loki solo rechaza una línea vieja por ir atrasada respecto a una más
nueva de su stream, que es lo que la segunda comprobación de arriba sigue
haciendo. Una release publicada en la hora anterior a una pasada la envía esa
pasada y otra vez la siguiente, la misma línea en el mismo instante, que Loki
guarda una sola vez. Una contribución se envía dos veces del mismo modo, y su
línea lleva el número de comentarios y el título del elemento, que pueden
moverse entre medias: medido contra Loki 3.7.7, una línea enviada otra vez con
`comments` pasado de 1 a 2 se guardó como una segunda línea en el mismo
instante. Con `-once` la cadencia que cuenta es la del calendario que lanza el
binario, así que pon `every.families.repo` y `every.families.outbound` a ese
valor. Las dos están en el almacén de métricas en cualquier caso.

## Para qué no sirve Loki

Para la historia fechada. De eso se encarga un almacén de métricas, y por eso
los dos van juntos y no uno en lugar del otro. Un log responde "qué pasó
recientemente, en orden"; una serie temporal responde "cuánto, en qué periodo".

## Los logs de jobs pertenecen aquí

La familia `joblogs`, apagada hasta que `every.families.joblogs` le da una
cadencia, recoge las últimas cuarenta líneas de cada job fallido de GitHub
Actions. Es texto y no una medida, así que el destino de InfluxDB lo excluye por
omisión y el exportador de Prometheus lo salta. Loki es donde le corresponde, y
la consulta es:

```text
{job="ghchronicle", kind="job_log"}
```

Los dashboards exportados no lo muestran, porque un dashboard atado a un
datasource no puede consultar dos y quien lo importa puede no tener Loki: llevan
un panel de texto, "Where failure output went", con esa consulta. Publicar el
dashboard desde el colector cambia ese panel por las líneas, las más recientes
primero, filtradas por la variable de repositorio del dashboard allí donde la
variable del almacén se puede leer como expresión regular. Basta con un sink de
Loki cuya dirección termine en `/loki/api/v1/push`: se adopta un datasource de
Loki que Grafana ya tenga en esa dirección, sin la ruta, y si no lo hay el
datasource se crea a partir de ella. Uno que escriba en otro sitio lo dice y
toma un `grafana.datasource.loki_uid` en su lugar, y lo mismo un Grafana que
llega a Loki por otra dirección. Un token que no puede crear el datasource
cuesta este panel y nada más: la ejecución avisa una vez y publica todos los
dashboards con la nota.

## Nada que migrar

Un stream es su job, su tipo y las etiquetas configuradas, y las etiquetas de
un punto van dentro de la línea, así que una versión que mueve una etiqueta no
cambia ningún stream, y [`-migrate`](https://docs.jmrp.io/ghchronicle/es/install/upgrading/#migraciones)
no tiene nada que hacer aquí. Una línea es lo que se dijo cuando se dijo, y
Loki rechaza de todos modos las entradas más antiguas que su ventana.

## Por dónde seguir

- [Elegir almacén](https://docs.jmrp.io/ghchronicle/es/sinks/) compara Loki con los demás, y
  lleva el registro de escrituras que todos comparten.
- [Los dashboards](https://docs.jmrp.io/ghchronicle/es/dashboards/) dice cuál de los cinco se dibuja
  contra cada almacén, y en qué se convierte un panel que un almacén no puede
  responder.
