# El fichero

La configuración de ghchronicle: un fichero YAML, sus credenciales, direcciones y rutas expandibles desde el entorno, y para qué sirve cada bloque de primer nivel.

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

ghchronicle lee un único fichero YAML, y el ejemplo documentado del repositorio es el punto de partida.

```sh
cp config.example.yaml config.yaml
```

`config.example.yaml` documenta cada opción en comentarios. Es el fichero que
hay que copiar, y se mantiene al día con el código: añadir un ajuste implica
añadirlo también allí.

## Los bloques

```yaml
github: # el token, la reserva, el timeout
targets: # qué repositorios
groups: # qué categorías de métrica llegan a recogerse
sinks: # a dónde van los puntos
every: # cada cuánto corre todo: un valor por defecto, por grupo, por familia
heartbeat: # cada cuánto despierta el bucle, para una prueba
log: # nivel, formato y un fichero rotatorio opcional
state_file: # lo que recuerda un reinicio
backfill: # el límite, al ejecutar con -backfill
migrate: # lo que hace un arranque con lo que una actualización dejó en los almacenes
grafana: # dónde se publica el dashboard, cuando se pide
```

Solo `github.token` y uno de `targets.user`, `targets.orgs` o `targets.repos`
son verdaderamente obligatorios, más al menos un destino. Todo lo demás tiene
valor por defecto.

## Expansión de `${VAR}`

Una credencial, una dirección o una ruta de fichero se expande desde el
entorno al arrancar. `${GITHUB_TOKEN}` se convierte en el valor de esa
variable, o en una cadena vacía si no está definida, salvo en una ruta de
fichero, más abajo.

```yaml
github:
  token: ${GITHUB_TOKEN}
sinks:
  influxdb:
    url: http://localhost:8181
    token: ${INFLUX_TOKEN}
```

La expansión va por clave, no sobre el fichero entero, y estas son las claves
a las que llega:

- credenciales: `github.token`, `sinks.influxdb.token`, cada valor de
  `sinks.otlp.headers`, `sinks.telegraf.username` y `password`,
  `sinks.postgres.dsn`, `sinks.elasticsearch.api_key`, `username` y
  `password`, y `grafana.token`, `user` y `password`;
- direcciones: `github.base_url` y `web_url`, la `url` de `influxdb`, `loki`,
  `telegraf` y `elasticsearch`, `sinks.otlp.endpoint`,
  `sinks.graphite.addr`, `grafana.url` y `grafana.datasource.url`;
- los identificadores de lo que Grafana ya tiene: `grafana.dashboard_uid`,
  `grafana.datasource.uid` y `grafana.datasource.loki_uid`;
- rutas de fichero: `state_file`, `sinks.dedupe_file`, `log.file`,
  `sinks.file.path` y `sinks.sql.path`, donde una `~` inicial es además el
  directorio personal, así que `state_file: ~/.ghchronicle/state.json` es un
  fichero dentro de él. Solo se lee así una `~` sola o seguida de un
  separador, `~nombre` se deja como está escrito, y una `~` cuando el proceso
  no tiene directorio personal se rechaza al arrancar nombrando la clave.
  También un `${VAR}` en una ruta que no está definido o está vacío, porque la
  ruta que queda sin él no es la escrita:
  `state_file: ${STATE_DIRECTORY}/state.json` sería `/state.json`, con el
  registro de escrituras y el fichero de caché a su lado en la raíz del sistema
  de ficheros. Antes de 2.6.1 no se expandía ninguna ruta, y esa `~` en un
  `state_file` era un directorio llamado `~` dentro del directorio de trabajo.

Cualquier otro valor se lee tal como está escrito, así que un `${VAR}` en
`sinks.loki.tenant_id`, `sinks.influxdb.bucket`, `sinks.prometheus.listen`,
una etiqueta, un prefijo o una cadencia le llega al colector como esos
caracteres.

Esta es toda la razón de que el fichero se pueda versionar. La configuración es
la forma del despliegue y le corresponde estar en control de versiones; los
tokens son credenciales y les corresponde un fichero de entorno con modo 600, o
un almacén de secretos.

- /etc/ghchronicle/
  - config.yaml legible por todos, versionado
  - ghchronicle.env modo 600, nunca versionado

## `github`

```yaml
github:
  token: ${GITHUB_TOKEN}
  reserve_rate: 500
  timeout: 30s
  # base_url: https://github.example.com/api/v3
  # web_url: https://github.example.com
```

| Clave          | Por omisión         | Significado                                                                                                                                              |
| -------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `token`        | obligatoria         | Un token personal clásico o fine-grained                                                                                                                 |
| `reserve_rate` | `500`               | Llamadas que no se gastan nunca, para que lo demás que use el token siga funcionando. Un valor no positivo cae al valor por defecto                      |
| `timeout`      | `30s`               | Por petición. GraphQL sobre una cuenta grande puede ser lento. Una duración de Go; lo que no se pueda interpretar o no sea positivo cae al valor por defecto |
| `base_url`     | api.github.com      | Una instancia de GitHub Enterprise usa `https://<host>/api/v3`. GitHub Enterprise Server no sirve el historial diario de estrellas, así que allí [`gh_star_day`](https://docs.jmrp.io/ghchronicle/es/collectors/measurements/#estrellas-y-forks) y los paneles que cuentan estrellas a partir de ella quedan vacíos |
| `web_url`      | derivada de la API  | El sitio donde está la página de perfil, que `achievements` lee sin el token. Solo hace falta cuando `base_url` es un proxy delante de la API |

La reserva se escala por cubo; ver [límites de la API](https://docs.jmrp.io/ghchronicle/es/api/).

## `state_file`

```yaml
state_file: /var/lib/ghchronicle/state.json
```

Nueve cosas, y borrar el fichero cuesta una distinta por cada una:

- `last_run`, cuándo corrió cada familia por última vez. Sin él todas las
  familias vencen a la vez, así que la pasada siguiente es completa, salvo que
  el servicio arranca las familias de seis horas o más
  [una por pasada](https://docs.jmrp.io/ghchronicle/es/configuration/cadences/#las-familias-lentas-se-turnan).
- `first_saw`, cuándo se recorrió entera por primera vez la lista de
  stargazers de cada repositorio. Solo se escribe tras un recorrido que volvió
  sin error. Sin él el recorrido completo de la lista de stargazers se vuelve a
  hacer.
- `history_read`, cuándo se leyó entero por última vez el historial diario de
  estrellas de cada repositorio. Solo se escribe tras un recorrido que llegó al
  final del historial, no tras uno que un error o un 403 o 404 de una página
  posterior cortó. Sin él ese historial se vuelve a leer entero, una página por cada
  treinta semanas de vida del repositorio.
- `last_head`, el commit en que estaba cada repositorio cuando corrió el diff
  de dependencias. Sin él los cambios de dependencias del hueco se pierden: la
  pasada siguiente tiene la fotografía y no el diff.
- `last_full`, cuándo hizo la lectura del día cada familia que normalmente lee
  solo lo que ha cambiado: para `issues`, cada elemento abierto y lo que se
  movió desde la lectura del día anterior, anotada por repositorio, así que uno
  que falló la repite y ningún otro. Sin él un repositorio se lee como vencido,
  así que la pasada siguiente la hace, llegando un mes atrás.
- `last_notified`, por dónde se cortó la ventana del buzón. Sin él a cero pide
  el buzón entero.
- `last_event`, el evento más reciente que tenía el feed. Sin él vacío lee el
  feed entero.
- `coauthored`, el recuento de Pair Extraordinaire que la familia
  `achievements` ha dado por cerrado, el último día UTC que cubre, la versión de
  la regla con la que se contó y el día en que se recorrió entero el historial
  por última vez. Cada pasada recorre solo las pull requests fusionadas desde
  ese día, y el historial entero otra vez una vez por semana o cuando la regla
  ha cambiado. Sin él la pasada siguiente recorre enteras las pull requests
  fusionadas de la cuenta, que en una cuenta con 2.315 fueron 35 consultas y
  23,7 MB, donde una pasada que lo tiene recorre solo el día en curso: la
  consulta de recuentos y una página del recorrido, dos puntos, y de 468 a
  513 KB en cada pasada horaria que registró el proxy de producción contra la
  misma cuenta el 2026-09-27, un tamaño que crece a lo largo del día con lo que
  se fusiona.
- `stores`, para cada almacén en el que escribe una ejecución, adónde apunta,
  la versión que escribió en él primero, la que lo hizo por última vez, las
  [migraciones](https://docs.jmrp.io/ghchronicle/es/install/upgrading/#migraciones) que se le han
  aplicado, las copias de filas antiguas que una migración apartó allí y el
  historial que una migración despejó allí y aún no ha vuelto a leer. Sin él un
  fichero SQL, un Graphite o un Telegraf se toman por de la versión en marcha,
  así que lo que dejó en ellos una versión anterior ya no se muestra, y un
  almacén despejado y no releído parece uno que nunca guardó la forma antigua,
  así que nada vuelve a leer su historial hasta que lo haga un
  `-backfill -families` de las familias que nombraba la relectura.

Siete de las nueve cuestan solo cuota, porque lo que se vuelve a recoger se
indexa por medida, etiquetas y marca de tiempo y sobrescribe lo que ya está
guardado. `last_head` pierde algo: los cambios de dependencias entre la cabeza
que tenía y la siguiente se leen de un rango que nadie puede nombrar una vez
que la cabeza ha desaparecido. También `stores`: qué versión escribió primero
en un almacén al que no se le puede preguntar cuál es la clave de sus filas, y
una relectura que una migración aún debe, son preguntas que nada puede
responder después.

Un fichero que está y no se puede leer ni analizar no se toma por uno nuevo:
una ejecución que escribe en los almacenes se detiene y lo nombra, porque leído
como nuevo olvidaría una relectura aún debida, y su primer guardado pondría un
fichero nuevo en su lugar. Una ejecución con otro usuario, root casi siempre,
es lo que lo deja ilegible; devuélveselo al usuario con el que corre el
colector. Una ejecución que guarda el fichero conserva lo que otro proceso
registró de los almacenes desde que ella lo leyó, así que dos ejecuciones que
se solapan, un `-once` de cron a la vez que un `-migrate -yes`, no deshacen
cada una los registros de los almacenes de la otra.

Una pasada con `-card-only` no escribe ninguna de las nueve. Sus puntos llegan a
[la tarjeta](https://docs.jmrp.io/ghchronicle/es/card/) y a ningún almacén, así que una marca suya
haría que la siguiente recogida se saltara una familia, o estrechara una
lectura, cuyos datos fueron a parar a una imagen y a ningún otro sitio. El
fichero lo lee como cualquier otra pasada.

### El otro fichero en el que una pasada se recuerda a sí misma

```yaml
sinks:
  dedupe_file: /var/lib/ghchronicle/state-written.bin
  dedupe_horizon: 720h
```

| Clave                  | Por omisión                                     | Significado                                                                        |
| ---------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------- |
| `sinks.dedupe_file`    | junto a `state_file`, como `<nombre>-written.bin` | El registro de lo que ya se ha escrito. `off` lo desactiva para todos los destinos |
| `sinks.dedupe_horizon` | `720h`                                          | Cuánto recuerda el registro un punto que ya nadie ofrece. Una duración de Go; lo que no se pueda interpretar o no sea positivo cae al valor por defecto |

Perderlo cuesta una pasada de reescritura y nada más, que es justo lo que quiere
un almacén que se ha vaciado y hay que volver a llenar. Los dos ficheros quieren
una ruta persistente:
[solo se escribe lo que ha cambiado](https://docs.jmrp.io/ghchronicle/es/sinks/#solo-se-escribe-lo-que-ha-cambiado).

### La caché que hay a su lado

```text
/var/lib/ghchronicle/state-cache.bin
```

Junto a `state_file`, como `<nombre>-cache.bin`, está lo que una pasada aprendió
de GitHub y que abarata la siguiente. No hay nada que configurar: su sitio es el
del fichero de estado. Guarda cuatro cosas:

- la [caché condicional](https://docs.jmrp.io/ghchronicle/es/api/#los-etags-y-por-qué-un-304-es-gratis):
  cada respuesta pedida dentro del doble de la cadencia más larga que corre la
  configuración, y nunca menos de un día, 48 horas con las cadencias por
  omisión, con el ETag con que llegó, así que un reinicio pregunta con
  `If-None-Match` y recibe un 304 gratis;
- las ejecuciones de workflow cuyos jobs ya se escribieron, para que un
  reinicio no los vuelva a listar;
- los [rechazos](https://docs.jmrp.io/ghchronicle/es/api/#un-rechazo-también-se-recuerda), cada uno
  hasta el final de su propio día;
- los tamaños de página que la familia `totals` dio a la consulta de pull
  requests.

Las cuatro vivían antes en el proceso, y cada reinicio las volvía a pagar.
Medido en el servicio del autor el 2026-09-26, los primeros 38 minutos tras un
reinicio gastaron 1.092 peticiones core cobradas en pasadas que costaban unas 66
con la caché caliente.

Las ejecuciones son la afirmación de que cada almacén tiene sus jobs, así que un
arranque no las recupera donde eso no se sabe: donde ningún registro de
escrituras recuerda lo que tienen los almacenes, que es lo que pasa al borrar el
registro para volver a llenar un almacén vaciado, con `dedupe_file: off` o con
el `dedupe: false` de un almacén, y en una ejecución que termina con su pasada;
y donde se ha añadido un destino desde que se escribió el fichero. Sus jobs se
vuelven a listar entonces y se ofrecen otra vez con todos los demás puntos. Dos
de esos casos son noticia, un registro que la configuración mantiene y que se
lee vacío y un destino añadido, y el arranque los dice:

```text
level=INFO msg="the write ledger remembers nothing, listing the jobs of the runs the cache file remembers again" runs=666
level=INFO msg="a store was added since the cache file was written, listing the jobs of the runs it remembers again" stores=influxdb,loki,postgres written_to=influxdb,loki runs=666
```

Los demás son como corre siempre la configuración, un almacén sin registro o
una ejecución que termina con su pasada, y solo se dicen en `debug`, como
`not every store keeps a write ledger`.

Cada arranque que encuentra el fichero dice lo que ha leído, en su primera
pasada, y uno que no encuentra ninguno lo dice en `debug`:

```text
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
```

Se escribe como mucho cada cinco minutos mientras el recolector corre, y una vez
más cuando se para: al final de un `-once`, y cuando al servicio se le pide
parar con `SIGTERM` o `SIGINT`, que es lo que envían `systemctl stop`,
`docker stop` y Ctrl-C. Un proceso matado sin más no puede, así que `SIGKILL`,
el OOM killer del núcleo, `docker kill`, `taskkill /F` y
`Stop-ScheduledTask` pierden lo que aprendió desde el último guardado, cinco
minutos como mucho, y el siguiente arranque vuelve a pedir esas respuestas. Un
guardado que falla lo dice, y también un arranque que encuentra un fichero que
no puede leer:

```text
level=WARN msg="cache file not saved" file=/var/lib/ghchronicle/state-cache.bin err="open /var/lib/ghchronicle/state-cache.bin.tmp: permission denied"
level=WARN msg="cache file not read, the first pass of each family pays in full" file=/var/lib/ghchronicle/state-cache.bin err="..."
```

Una pasada con `-card-only` lo lee y no lo escribe, por la misma razón por la
que deja en paz el fichero de estado.
Un `-backfill` lo lee y tampoco lo escribe: las páginas que recorre son páginas
que ninguna pasada pide, y guardadas desplazarían del fichero las respuestas de
las propias pasadas. Una actualización lo conserva: una respuesta se guarda bajo
la forma de lo que lee su recolector, así que las únicas respuestas que se
vuelven a pedir enteras son las de un recolector que ahora lee otra cosa.

Está acotado. Una respuesta de más de un megabyte se queda fuera, y también lo
que pase de 64 MB, empezando por lo que menos recientemente se pidió. Medido
contra la cuenta del autor, de 37 repositorios, una pasada de todas las familias
guarda 1.065 respuestas, 10,3 MB de ellas y 1,6 MB de fichero, ninguna de más de
404 KB. Con los días la caché en memoria crece con las respuestas cuya consulta
lleva una ventana móvil: el servicio del autor tenía 6.695 tras cinco días y
medio, unos 99 MB, y las 2.517 de ellas pedidas en las últimas 48 horas, que son
las que guarda el fichero, suman unos 31 MB. Contiene lo que GitHub respondió
sobre repositorios privados además de públicos, y se escribe con modo 600, como
el fichero de estado.

Borrarlo cuesta una pasada de cada familia a precio completo, el precio que
pagaba cada reinicio antes de que existiera, y no pierde nada. Cada respuesta se
vuelve a pedir, cada función rechazada se vuelve a preguntar, que es también la
manera de que una función activada hoy se note antes de que acabe su día, los
jobs de las ejecuciones más recientes se vuelven a listar una vez, y la primera
pasada corre `totals` antes de las pull requests, diga lo que diga su cadencia,
así que sus páginas se vuelven a dimensionar. Cuando `totals` no tocaba de
todos modos, lo dice:

```text
level=INFO msg="no page sizes remembered, running totals before the pull requests it sizes"
```

Un fichero que no se carga entero, cortado, dañado o escrito en otro formato,
no se lee en absoluto, con el aviso de arriba, y cuesta lo mismo; el siguiente
guardado lo sobrescribe. Quiere la misma ruta persistente que los otros dos.

## `backfill`

```yaml
backfill:
  since: 2y
```

Solo se aplica a una ejecución lanzada con `-backfill`, y `-backfill-since` lo
sobrescribe. Ver [relleno histórico](https://docs.jmrp.io/ghchronicle/es/how/backfill/).

Un relleno lleva un fichero más junto a `state_file`,
`<nombre>-progress.json`, donde anota lo que ya ha escrito para que una parada
cueste un repositorio y no el recorrido entero. No hay nada que configurar:
aparece cuando arranca un relleno, se borra cuando el recorrido llega al final,
y una pasada normal ni lo escribe ni lo lee. Ver [pararlo, y
retomarlo](https://docs.jmrp.io/ghchronicle/es/how/backfill/#pararlo-y-retomarlo).

## `migrate`

```yaml
migrate: auto
```

Lo que hace una ejecución que escribe en los almacenes, antes de su primera
pasada, con un almacén que una versión anterior dejó con una forma que esta ya
no escribe. `-migrate` imprime lo que cada almacén configurado guarda de cada
cambio así; ver [Migraciones](https://docs.jmrp.io/ghchronicle/es/install/upgrading/#migraciones).

| Valor  | Qué hace un arranque                                                                                                |
| ------ | ------------------------------------------------------------------------------------------------------------------- |
| `auto` | El valor por omisión. Aplica por su cuenta un cambio pendiente que no pierde nada, lo dice en `WARN`, vuelve a leer lo que despejó, y lo que una ejecución anterior despejó y no terminó de leer, y avisa de todos los demás en cada arranque |
| `warn` | No aplica nada ni vuelve a leer nada, y avisa de cada cambio pendiente, y de cada almacén al que aún se le debe su historial, en cada arranque |

Un cambio no pierde nada cuando se cumplen las tres cosas: GitHub todavía sirve
toda la historia, así que volver a leerla trae de vuelta cada fila que guarda
el almacén; las filas antiguas se apartan al menos 24 horas en vez de borrarse,
que es lo que hacen InfluxDB 3, PostgreSQL y Elasticsearch; y todas las filas
del almacén son de esta configuración, lo que se le pregunta al almacén. No hay
valor que aplique el resto por su cuenta. Una ejecución de una sola vez,
`-once` o `-backfill`, sobre un fichero de estado nuevo tampoco aplica nada por
su cuenta: esa es cada ejecución de la Action sin un fichero de estado
restaurado, cuyo registro de una relectura aún debida se va con su runner.
Cada aviso nombra las dos órdenes, `-migrate` para el plan y `-migrate -yes`
para aplicarlo, con el servicio parado. Cualquier otro valor se rechaza al
arrancar.

El servicio también tiene un bloqueo junto al fichero de estado,
`<nombre>-lock`, mientras corre, que es lo que impide que `-migrate -yes`, y
`-uninstall -yes` de los datos o del estado, cambien los almacenes bajo sus
pies. No hay nada que configurar: sigue a
`state_file`, el sistema operativo lo suelta acabe como acabe el proceso, y el
fichero que queda solo nombra el último proceso que lo tuvo.

## Cuando está mal, lo dice al arrancar

La configuración se valida antes de hacer la primera llamada, y los mensajes
nombran la clave y lo que necesita.

| Mensaje                                                               | Significa                                                          |
| --------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `github.token is empty and GITHUB_TOKEN is unset`                     | Exactamente lo que dice                                            |
| `state_file: "${X}/state.json" names ${X}, which is unset, ...`       | Una ruta nombra una variable sin valor: defínela, o escribe la ruta entera |
| `targets: set at least one of user, orgs or repos`                    | No hay nada que recoger                                            |
| `sinks: enable at least one of ...`                                   | Una ejecución que recoge y tira casi nunca es lo que alguien quiso |
| `every.families.<name>: unknown collector`                            | El nombre no es una familia. El mensaje lista las que existen      |
| `sinks.influxdb: url and bucket are required`                         | Cada destino valida sus propias claves obligatorias y dice cuáles  |
| `sinks.sql.dialect: "mysql" is not postgres, the only dialect so far` | El valor no es uno de los aceptados, y el mensaje los lista        |
| `migrate: "off" is not auto or warn`                                  | Lo mismo, para `migrate`                                           |

> **Una cadencia para una familia inexistente es fatal**
>
> `every` se comprueba contra la lista de colectores conocidos al arrancar en
> vez de ignorarse. De lo contrario, una errata ahí significaría una familia
> corriendo en silencio con su valor por defecto para siempre, algo que ya ha
> mordido dos veces.

## El resto

- [Objetivos](https://docs.jmrp.io/ghchronicle/es/configuration/targets/): qué repositorios, y los
  valores por defecto de forks y archivados.
- [Cadencias](https://docs.jmrp.io/ghchronicle/es/configuration/cadences/): el bloque `every`, y qué
  significa `0`.
- [Registro](https://docs.jmrp.io/ghchronicle/es/configuration/logging/): nivel, formato y el
  fichero rotatorio.
- [Elegir almacén](https://docs.jmrp.io/ghchronicle/es/sinks/): el bloque `sinks`, una página por
  almacén.
- [Dejar que el binario publique el dashboard](https://docs.jmrp.io/ghchronicle/es/dashboards/#dejárselo-al-binario):
  el bloque `grafana`.
