# Registro

Nivel, formato, el fichero rotatorio que nunca sustituye a la salida de error, y las dos líneas que merecen alerta.

Source: https://docs.jmrp.io/ghchronicle/es/configuration/logging/

ghchronicle escribe su registro en la salida de error, y el bloque `log` fija el nivel, el formato y un fichero rotativo opcional.

```yaml
log:
  level: info # debug, info, warn, error
  format: text # text o json
  file: /var/log/ghchronicle/ghchronicle.log
  max_bytes: 67108864
  keep: 5
```

## Las claves

| Clave       | Por omisión | Significado                                           |
| ----------- | ----------- | ----------------------------------------------------- |
| `level`     | `info`      | `debug`, `info`, `warn` o `error`; cualquier otro valor se comporta como `info` |
| `format`    | `text`      | `text` para una persona, `json` para un agente de envío; cualquier otro valor es `text` |
| `file`      | ninguno     | Un fichero rotatorio **además de** la salida de error |
| `max_bytes` | `67108864`  | Rota a los 64 MiB. Un valor no positivo cae al valor por defecto |
| `keep`      | `5`         | Cuántos ficheros rotados conservar. Un valor no positivo cae al valor por defecto |

## Los dos, nunca en lugar de

Un `file` configurado no sustituye a la salida de error, se escribe además de
ella. Bajo systemd el journal es donde mira todo el mundo primero, y un fichero
de log que ocupara en silencio el lugar del journal sería una trampa:
`journalctl -u ghchronicle` se quedaría mudo y la conclusión obvia sería que el
servicio se ha parado.

La rotación es por tamaño con sufijos numerados, así que la retención es un
recuento y no una fecha y dos rotaciones en el mismo segundo no pueden chocar. El
contador de tamaño se lee del fichero al arrancar, así que un reinicio no lo
pone a cero dejando que el fichero crezca sin límite.

## Qué dice el log un día bueno

Tomado del servicio del autor el 2026-09-27, un arranque y las líneas de sus
pasadas:

```text
level=INFO msg="ghchronicle running" tick=15m0s tick_from="shortest cadence"
level=INFO msg="cache file read" file=/var/lib/ghchronicle/state-cache.bin written=2026-09-27T15:27:41+02:00 answers=1170 runs=672 refusals=100 page_sizes=54 age=1s
level=INFO msg="repositories discovered" count=37 archived_aside=17
level=INFO msg="slow families due together take turns" starting=planning waiting=settings,traffic
level=INFO msg=written sink=influxdb family=repo points=934 unchanged=1955
level=INFO msg="nothing moved since the window, repositories left unread" family=commits repos=34
level=INFO msg=written sink=influxdb family=commits points=17 unchanged=0
level=INFO msg="rate budget" bucket=core remaining=4477 limit=5000
level=INFO msg="points already written and not sent again" sink=influxdb skipped=54511
level=INFO msg="sweep finished"
```

Las dos primeras salen una vez, cuando arranca el proceso: el tic al que
despierta el bucle, y lo que devolvió [la caché que hay junto al fichero de
estado](https://docs.jmrp.io/ghchronicle/es/configuration/#la-caché-que-hay-a-su-lado). La lista de
repositorios se rehace una vez por hora, y `archived_aside` cuenta los
archivados [apartados](https://docs.jmrp.io/ghchronicle/es/configuration/targets/#por-qué-forks-y-archivados-están-apagados-por-omisión),
que siguen recibiendo sus dos filas. En `written`, `points` es lo que llegó al
almacén y `unchanged` lo que el registro de escrituras dejó fuera porque el
almacén ya lo tiene; ver [qué cuenta la línea de
escritura](https://docs.jmrp.io/ghchronicle/es/sinks/#qué-cuenta-la-línea-de-escritura). Una
familia de seis horas o más a la que le toca detrás de otra aparece en
`waiting`, ver [las familias lentas se
turnan](https://docs.jmrp.io/ghchronicle/es/configuration/cadences/#las-familias-lentas-se-turnan),
y `commits`, `issues` e `issueevents` dicen cuántos repositorios dejaron sin
leer porque nada se movió en ellos, ver [preguntar primero qué se
movió](https://docs.jmrp.io/ghchronicle/es/api/cost/#preguntar-primero-qué-se-movió).

Una familia que aún no toca sencillamente no aparece. Eso es normal, y es lo
primero que hay que mirar cuando parece faltar una medida: con una cadencia de
doce horas, medio día de logs puede legítimamente no mencionar nunca `stats`.

## Las dos líneas que merecen alerta

**`rate limit reserve reached`** significa que se saltó una familia para
proteger el presupuesto.

```text
level=WARN msg="rate limit reserve reached, family skipped" family=artifacts
```

Una vez está bien. En cada pasada significa que las cadencias son demasiado
rápidas para el número de repositorios.

**`family failed everywhere, not marking it as run`** significa que fallaron
todos los repositorios en una familia.

```text
level=WARN msg="family failed everywhere, not marking it as run" family=security
```

La familia no se marca como hecha a propósito, así que se reintenta en la
siguiente cadencia en vez de darse por completa. Esa es la línea que separa "una
función está apagada en un repositorio" de "el token ha perdido un permiso".

## Depuración

```yaml
log:
  level: debug
```

Debug añade estas líneas, y ninguna más:

- `loaded the written-points ledger`, con cuántos puntos volvió el libro de
  escrituras, cuando arranca el servicio;
- `no cache file yet, the first pass of each family pays in full`, en el
  primer arranque con fichero de estado;
- `not every store keeps a write ledger`, cuando las ejecuciones que recuerda
  el fichero de caché se vuelven a listar porque un almacén no lleva registro
  o la ejecución termina con su pasada;
- `no targets.user, account-wide families skipped`;
- `sink dropped old entries`, con cuántas entradas dejó fuera un destino por
  ser más antiguas que su horizonte, la única forma de ver que un envío se
  recortó en lugar de rechazarse;
- `card-only sweep, the state file is left as it was`;
- `cache file saved`, con cuántas respuestas y ejecuciones guarda y cuánto
  tardó;
- `migration not needed` y `migration applied before`, en cada arranque, por
  cada cambio que un almacén nunca necesitó o que ya tiene aplicado;
- `migration noted: nothing is changed` y `migration frozen`, en cada arranque
  después del primero, que dice cada una en `INFO`;
- `set-aside copy forgotten`, cuando le llega su hora a una copia que apartó una
  migración y el fichero de estado deja de nombrarla: `the store purges it
  itself` para una que InfluxDB 3 purga con su propio calendario, y `the store
  keeps it for good, and -migrate asks the store for it` para una que guarda
  para siempre.

No hay registro por petición: el cliente de `internal/ghapi` no lleva
registrador, así que qué endpoint se llamó y qué respuesta volvió 304 no se ven
en ningún nivel. Eso incluye su único reintento: una petición REST respondida
con 500, 502, 503 o 504 se repite dos segundos después, en silencio, y también
la descarga del log de un job desde el almacenamiento, así que un
`collector failed` que nombra uno de esos cuatro ya ha fallado dos veces.

> **JSON para un agente de envío, texto para una persona**
>
> `format: json` emite un objeto por línea, que es lo que quieren Promtail,
> Vector o Filebeat. Es la misma información; solo cambia la codificación.
> Combinar `format: json` con un `file` es la disposición para una máquina que
> ya envía logs a algún sitio.

## No confundir con el colector de logs de jobs

`log` es el diario de la propia herramienta. `every.families.joblogs` es un _colector_:
recoge las últimas cuarenta líneas de cada job fallido de GitHub Actions y las
convierte en puntos. Son ajustes sin relación, y el segundo pertenece a un
almacén de logs y no a una base de datos de métricas. Ver
[Loki](https://docs.jmrp.io/ghchronicle/es/sinks/loki/).
