# Elasticsearch

La API bulk, un índice por medida, y un id de documento que hace que una reescritura reemplace en vez de duplicar.

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

El destino Elasticsearch envía los puntos fechados de ghchronicle a Elasticsearch u OpenSearch en bloque, un documento por punto.

```yaml
sinks:
  elasticsearch:
    url: http://elasticsearch:9200
    prefix: ghchronicle
    api_key: ${ES_API_KEY} # o username y password
    batch: 1000
```

La API `_bulk`, que comparten Elasticsearch y OpenSearch, así que el mismo
destino sirve a ambos, y a Kibana o a los dashboards de OpenSearch encima de
cualquiera de los dos.

## Credenciales

- **Clave de API**

  ```yaml
  api_key: ${ES_API_KEY}
  ```

  Se envía como `Authorization: ApiKey`.

- **Básica**

  ```yaml
  username: ghchronicle
  password: ${ES_PASSWORD}
  ```

  Se envía como autenticación básica.

No las dos. Configurar una clave de API junto a un usuario falla la validación
al arrancar con `sinks.elasticsearch: set either api_key or username and
password, not both`.

## Los documentos

Un índice por medida, llamado `<prefijo>-<medida>`: `ghchronicle-gh_repo`,
`ghchronicle-gh_traffic`.

Un documento por punto, con la hora como `@timestamp` en RFC 3339,
`measurement`, y cada etiqueta y cada campo como clave de primer nivel, para que
no haya que desanidar nada antes de poder filtrar por ello.

```json
{
  "@timestamp": "2026-09-07T00:00:00Z",
  "measurement": "gh_traffic",
  "owner": "acme",
  "repo": "telemetry",
  "full_name": "acme/telemetry",
  "kind": "views",
  "count": 220,
  "uniques": 131
}
```

## Por qué aquí también converge volver a recoger

El id del documento es el **SHA-256 de la medida, las etiquetas que tienen valor
y la marca de tiempo**, y la acción es `index` y no `create`. Así que escribir la
misma ventana de tráfico de catorce días cada seis horas reemplaza catorce
documentos en vez de añadir otros catorce, que es la misma convergencia que
InfluxDB da de balde.

> **Una petición bulk responde 200 aunque fallen elementos**
>
> Elasticsearch informa de los veredictos por elemento dentro de una respuesta
> correcta. El destino los lee, registra cada documento rechazado con la razón
> del propio clúster e informa la cuenta como aviso y no como fallo, porque todo
> lo demás del lote se escribió.

## No se escribe ningún mapping

El destino no crea plantilla de índice. El mapping dinámico da a cada campo de
texto un subcampo `.keyword`, que es sobre lo que agrega el dashboard.

Si quieres mappings explícitos, crea las plantillas de índice antes de la
primera escritura. Nada del destino depende de ellas; solo la elección de
`.keyword` en los paneles. [`-migrate`](https://docs.jmrp.io/ghchronicle/es/sinks/elasticsearch/#qué-hace-aquí-una-migración) lee el
mapping antes de preguntar de quién son las filas de un índice, así que una
plantilla que mapea las cadenas como `keyword`, sin subcampo `.keyword`, se lee
del propio campo.

## El dashboard

`dashboards/ghchronicle-elasticsearch.json` tiene los mismos 154 paneles que el
de InfluxDB, como filtros Lucene y agregaciones sobre **un solo** datasource
apuntando a `<prefijo>-*`, porque cada objetivo nombra su propio índice en su
consulta.

Pon el campo de tiempo del datasource a `@timestamp`. Una tabla por elemento
allí son los documentos más nuevos en sí, o el más nuevo de cada elemento
cuando un elemento tiene varios; todo lo demás es una agregación por cubos.
OpenSearch funciona con el mismo plugin.

Hay dos cosas que una consulta no puede hacer allí, y los paneles que las
necesitan lo dicen. No puede unir dos índices, así que Work elsewhere no tiene
columna Stars y el perfil de comunidad conserva la bandera de plantilla de
issue de la API en vez del número de plantillas. Y no puede preguntar por una
ventana mientras lee otra: el Overview y Every repository, ever cuentan un
repositorio archivado que el filtro por omisión aparta por sus documentos de
los últimos siete días, donde los almacenes SQL preguntan si el colector lo
sigue escribiendo, así que un rango que terminó hace más de una semana deja
fuera esos repositorios.

## Qué hace aquí una migración

Cuando una versión cambia aquello por lo que se identifican los documentos de
una medida, [`-migrate`](https://docs.jmrp.io/ghchronicle/es/install/upgrading/#migraciones)
encuentra los documentos de la forma antigua contando los que llevan la
etiqueta antigua, y aplicar el cambio aparta el índice en tres pasos.
**Medido contra Elasticsearch 9.5.3**:

1. El índice deja de aceptar escrituras, con `PUT <índice>/_block/write`.
2. Se clona a `<índice>-<instante>`, el instante en UTC y en minúsculas, como
   exige el nombre de un índice, por ejemplo
   `ghchronicle-gh_discussion_comment-20261001t091004`, y el clon tiene que
   tener tantos documentos como el índice antes de que ocurra nada más.
3. Se borra el índice, y la siguiente escritura del destino lo crea de nuevo,
   con un mapping propio.

Antes de aplicar el cambio, el plan pregunta de quién son las filas del índice
y a qué repositorios pertenecen, con una agregación terms sobre el campo que el
mapping deje agregar: el subcampo `.keyword` que da el mapping dinámico a una
cadena, o el propio campo donde una plantilla lo mapea como `keyword`. Los
buckets tienen que sumar todos los documentos que tienen el campo, o la
respuesta no se acepta: medido en 9.5.3, una agregación sobre `user.keyword` en
un índice cuya plantilla mapeaba `user` como `keyword` respondió sin buckets y
sin error, lo que se leía como un índice sin filas de nadie más, y un valor que
pasa del `ignore_above` del subcampo tampoco está en ningún bucket. Un
documento sin el campo es una fila que no nombra a nadie, y el plan lo dice
así.

Un paso que falla antes del borrado vuelve a quitar el bloqueo de escritura,
así que un clúster que rechazó el clon sigue aceptando las escrituras del
destino, y el cambio sigue pendiente con la razón del clúster. Un borrado que
falla se vuelve a mirar antes de deshacer nada: medido a través de un proxy que
respondió 502 a un borrado que el clúster llevó a cabo, el índice ya no
estaba, así que el clon, la única copia de sus documentos, se conserva y el
cambio se registra como aplicado; solo a un índice que sigue ahí se le quita el
bloqueo y se le borra el clon. La clave de API
necesita los privilegios `manage` y `delete_index` sobre los índices del
prefijo; una clave que solo puede escribir se rechaza así.

El clon conserva el bloqueo de escritura, y ghchronicle lo borra cuando lleva
24 horas guardado: tras una pasada del servicio, en el siguiente arranque de
cualquier ejecución o en el siguiente `-migrate -yes`. Hasta entonces, deshacer
el cambio es borrar el índice nuevo y clonar la copia de vuelta con su nombre.
Toda consulta del dashboard que se distribuye nombra su índice entero,
`_index:ghchronicle-gh_discussion_comment`, así que ningún panel lee el clon:
la consulta de un panel sobre `ghchronicle-*` contó el único documento nuevo y
no los tres del clon. Una data view de Kibana sobre el mismo patrón sin ese
filtro sí lo lee, durante el día que se guarda.

## Por dónde seguir

- [Elegir almacén](https://docs.jmrp.io/ghchronicle/es/sinks/) compara Elasticsearch 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.
