Analytics Engine: telemetría a medida con SQL
Escribir tus propios eventos y métricas desde el Worker y consultarlos con SQL: declarar el binding, entender la anatomía de un data point (blobs, doubles, indexes), y por qué el muestreo adaptativo obliga a leer siempre a través del sample_interval para no mentirte en las cuentas.
Los logs responden a la pregunta “¿qué pasó en esta invocación concreta?”. Pero hay una pregunta distinta, que ningún log responde bien: “¿cuál es la forma agregada de todas mis invocaciones?” —cuántas por ruta, qué latencia media por país, cuántos pedidos por cliente y hora—. Para eso existe Workers Analytics Engine: un almacén de series temporales de alta cardinalidad al que tu Worker escribe eventos propios casi gratis, y que consultas después con SQL. Es la pieza que te convierte de consumidor de métricas ajenas en autor de tu propia telemetría: mides exactamente lo que tu negocio necesita medir, no lo que un panel prefabricado decidió mostrarte.
- Distinguir un log (un evento que lees) de una métrica (un agregado que computas).
- Declarar el binding de Analytics Engine y escribir data points desde el Worker.
- Dominar la anatomía de un data point:
indexes,blobsydoubles. - Consultar con SQL y leer siempre a través del
_sample_intervalpara no falsear las cuentas.
Logs y métricas son dos preguntas distintas
Un log es un registro puntual: cuenta la historia de una invocación y se lee de una en una. Una métrica es un agregado: no te importa un evento suelto, te importa la suma, la media, el percentil de millones de ellos. Podrías intentar reconstruir métricas releyendo logs, pero es caro, lento y su retención corta no llega. Analytics Engine invierte el enfoque: escribes datos pensados desde el principio para ser agregados, y los consultas con la herramienta natural de la agregación —SQL—.
Su superpoder es la alta cardinalidad. Los sistemas de métricas tradicionales colapsan cuando una etiqueta tiene demasiados valores distintos (un identificador de cliente, una URL única): cada combinación crea una serie nueva y el coste explota. Analytics Engine está diseñado para tragarse esa cardinalidad sin miedo, lo que te permite medir por cliente, por ruta o por cualquier dimensión granular que tu negocio necesite.
El binding y la escritura
Como todo recurso en el edge, Analytics Engine se accede por un binding declarado en wrangler.jsonc, no por credenciales:
{
"analytics_engine_datasets": [
{ "binding": "AE", "dataset": "metricas_app" }
]
}
Desde el Worker, escribir un evento es una sola llamada sobre env.AE:
export default {
async fetch(request, env, ctx) {
const inicio = Date.now();
const url = new URL(request.url);
const pais = request.cf?.country ?? "XX";
// ... la lógica del Worker ...
env.AE.writeDataPoint({
indexes: [url.pathname], // clave de muestreo (1 como maximo)
blobs: [pais, request.method], // dimensiones: hasta 20 cadenas
doubles: [Date.now() - inicio], // metricas: hasta 20 numeros
});
return new Response("ok");
},
};
La escritura es no bloqueante y baratísima: no consume tu tiempo de CPU, no cuenta como subrequest y no añade latencia perceptible a la respuesta. Puedes instrumentar generosamente sin miedo a degradar el Worker. Esta es precisamente la propiedad que la distingue de un log costoso o de una escritura a base de datos: mides mucho, pagas casi nada. Instrumenta primero y optimiza después, porque aquí instrumentar es prácticamente gratis.
La anatomía de un data point
Un data point tiene tres compartimentos, y elegir bien qué va en cada uno es el arte de esta herramienta:
blobs
Hasta 20 cadenas de texto: las dimensiones o etiquetas por las que agruparás y filtrarás —ruta, país, método, nombre de evento—. Son el eje cualitativo de tus consultas.
doubles
Hasta 20 números: las magnitudes que medirás —duración en milisegundos, bytes servidos, importe—. Son lo que sumas, promedias y percentilas.
indexes
Uno como máximo: la clave sobre la que Analytics Engine decide el muestreo. Ponle la dimensión de mayor cardinalidad y mayor peso analítico, como el identificador de cliente.
El orden importa y es posicional: el primer blob será blob1 en SQL, el primer double será double1, el único indexes será index1. No hay nombres de columna; hay posiciones. Documenta en tu código qué mide cada posición, porque en la consulta no habrá etiquetas que te lo recuerden.
Consultar con SQL y el _sample_interval
Se consulta por una API HTTP a la que envías SQL como cuerpo de la petición, autenticada con un token de API:
curl "https://api.cloudflare.com/client/v4/accounts/TU_ACCOUNT_ID/analytics_engine/sql" \
-H "Authorization: Bearer TU_TOKEN" \
-d "SELECT blob1 AS pais, SUM(_sample_interval) AS peticiones
FROM metricas_app
WHERE timestamp > NOW() - INTERVAL '1' DAY
GROUP BY pais ORDER BY peticiones DESC"
Aquí aparece el concepto que no puedes ignorar: el muestreo adaptativo. Cuando el volumen es alto, Analytics Engine no guarda todos los eventos, sino una muestra, y anota en cada fila cuántos eventos reales representa a través de la columna _sample_interval. Si una fila tiene un _sample_interval de 100, significa “esta fila vale por 100 eventos que ocurrieron de verdad”.
La consecuencia es una regla de oro: nunca cuentes filas, cuenta a través del intervalo de muestreo. Un COUNT(*) te dice cuántas filas guardó Cloudflare, no cuántos eventos ocurrieron. La verdad se obtiene ponderando:
-- MAL: cuenta filas muestreadas, no la realidad
SELECT blob1 AS ruta, COUNT(*) FROM metricas_app GROUP BY ruta;
-- BIEN: estima el total real ponderando por el intervalo
SELECT blob1 AS ruta,
SUM(_sample_interval) AS peticiones_reales,
SUM(_sample_interval * double1) AS suma_duracion_real
FROM metricas_app
WHERE timestamp > NOW() - INTERVAL '1' HOUR
GROUP BY ruta ORDER BY peticiones_reales DESC;
flowchart LR W[Worker writeDataPoint] --> AE[dataset de Analytics Engine] AE -->|muestreo adaptativo| S[filas con sample_interval] S --> API[SQL API por HTTP] API --> Q[SUM sample_interval estima el total real] Q --> G[panel o Grafana] style W fill:#89b4fa,color:#11111b style S fill:#f9e2af,color:#11111b style Q fill:#a6e3a1,color:#11111b
Toda medición perturba y toda medición cuesta; la fantasía de una observabilidad total, que registre cada evento sin pagar nada, es exactamente eso, una fantasía. Los sistemas serios no la persiguen: la sustituyen por una honestidad. Analytics Engine encarna esa honestidad en una sola columna, el _sample_interval, que es la contabilidad explícita de lo que no guardó. En lugar de fingir que lo registra todo —y colapsar, o mentirte con números incompletos que parecen completos—, guarda una muestra y te dice con precisión cuánto representa cada fila, para que tú reconstruyas la verdad ponderando. Ahí se esconde una lección que trasciende a Cloudflare: en cualquier sistema a escala real la telemetría es siempre una muestra, lo sepas o no, y la única diferencia entre una métrica fiable y una engañosa es si el sistema te confiesa su muestreo o te lo oculta. El ingeniero ingenuo hace COUNT(*) y confía en el número; el ingeniero maduro sabe que ese número cuenta filas almacenadas, no realidad ocurrida, y multiplica por el intervalo para recuperar la escala verdadera. Esa diferencia —entre leer el dato crudo y leerlo a través de su factor de muestreo— es la frontera entre creerte tus paneles y entenderlos. Y hay un giro final: al escribir tus propios data points dejas de ser consumidor de la telemetría que un proveedor decidió por ti y pasas a ser su autor. Eliges qué dimensiones importan, qué magnitudes mides, qué clave gobierna el muestreo. La observabilidad deja de ser algo que te dan y se convierte en algo que diseñas, y diseñar bien qué medir —y cómo leer honestamente lo que mediste— es una competencia de ingeniería tan seria como diseñar el propio sistema.
- Declara un binding de Analytics Engine en
wrangler.jsoncy escribe unwriteDataPointpor invocación con al menos unindex, dosblobsy undouble. - Documenta en un comentario qué mide cada posición (
blob1,double1,index1), porque el SQL no te lo recordará. - Genera tráfico variado y lanza una consulta que agrupe por
blob1y estime el total real conSUM(_sample_interval). - Compara ese resultado con un
COUNT(*)sobre la misma consulta y explica por qué difieren cuando hay muestreo. - Añade una segunda métrica ponderada, como la duración media real, y razona por qué debes multiplicar el
doublepor el_sample_intervalantes de promediar.