Documentación
Estiaje recibe lo que pasó en tu código y lo que le importa a tu negocio en un mismo río. Esta página es todo lo que necesitas: se lee en quince minutos y no hay una segunda parte escondida.
Instalar
npm install @estiaje/sdk
Cero dependencias. Node 18 o superior. Funciona igual en una API, en un front empaquetado y en una lambda.
Tu primer evento
Consigue tu llave
Crea tu organización en la pantalla de alta. Te
devuelve una llave ck_live_… que se muestra una sola vez:
se guarda cifrada, así que si la pierdes se revoca y se genera otra.
Apunta el SDK
ESTIAJE_URL=https://tu-estiaje.example.com
ESTIAJE_TOKEN=ck_live_…
Sin ESTIAJE_URL el SDK queda apagado — todas las funciones
existen y no hacen nada. Cero red, cero buffers. Puedes instrumentar
producción hoy y decidir el destino después.
Emite
import { estiajeFromEnv } from '@estiaje/sdk'; // UNO por proceso. No uno por request, ni uno por archivo. export const estiaje = estiajeFromEnv({ service: 'mi-api' }); estiaje.log('usecase.ok', { usecase: 'GetVisits', ms: 128 });
Ábrelo en el río y verás caer el evento en menos de dos segundos.
Los tres verbos
La diferencia no es de estilo: decide cuánto vive cada cosa y qué preguntas vas a poder contestar dentro de un año.
| Verbo | Para qué | Vive |
|---|---|---|
estiaje.log() | Lo que pasó. Diagnóstico, tiempos, pasos internos. | Se borra: es ruido con fecha de caducidad. |
estiaje.event() | Lo que le importa al negocio: una visita empezó, un planograma cambió. | Se conserva. |
estiaje.outcome() | Lo que TERMINA un proceso: la venta se procesó, la tarea cerró. | Se conserva. Es la desembocadura. |
También hay warn(), error() y fatal():
son log con severidad.
log(), no te falta nada. Los otros dos verbos
aparecen el día que quieras contestar "¿cuántos pedidos terminaron en cobro
procesada?" sin abrir la base de datos.
Nombra por lo que ocurrió, no por dónde ocurrió
order.created, no PedidoController.done. El nombre
sobrevive al refactor; la ruta del archivo, no. Y en los eventos de negocio
incluye siempre el identificador de la cosa (task_id,
order_id, sn): es lo que permite emparejar el
inicio con el final.
Contexto y linaje
El hilo, la entidad, el actor y las etiquetas del request no se escriben en cada llamada. Se declaran una vez y se pegan solos a todo lo que se emita.
estiajeFromEnv({ service: 'api-pedidos', context: () => ({ trace_id: hiloActual(), // del interceptor / AsyncLocalStorage entity: 'sucursal:polanco', // el ancla: la cosa del mundo real actor: usuarioActual(), attrs: { session_id, module: 'checkout' }, geo: posicionActual(), }), })
Si el front manda su cabecera traceparent y tu interceptor la
respeta, la misma traza va del click del usuario hasta la lambda sin que
ningún caso de uso escriba un trace_id jamás. Eso es lo que dibuja
el viaje.
context() se llama en cada emisión. Si lanza, se
ignora — el SDK nunca falla por culpa del contexto.
Tus etiquetas nunca chocan con las del contexto
Si ambos traen la misma clave con distinto valor, gana la tuya y la del
contexto se conserva con el sufijo _ctx. Nada se pisa en silencio.
| Caso | Resultado |
|---|---|
| Solo el contexto la trae | queda la del contexto |
| Solo tú la escribes | queda la tuya |
| Ambos, mismo valor | se funden en una |
| Ambos, valores distintos | manda la tuya, la otra queda como <clave>_ctx |
Etiquetas (attrs)
Los attrs son pares clave/valor con los que después vas a buscar.
Pueden ser texto, número o booleano — y si mandas otra cosa, el SDK no tira
el evento: lo degrada con gracia y lo marca.
| Le mandas | Guarda |
|---|---|
Error | "TypeError: x is not a function" + marca _coerced |
Date | ISO 8601 |
| objeto / array | JSON truncado a 500 caracteres + _coerced |
| objeto circular | "[no serializable]" |
NaN / Infinity | su texto + _coerced |
null / undefined | se omite (sin ruido) |
El atributo _coerced lista qué claves se degradaron. La idea es que
el dato sucio sea visible y corregible, no invisible. Si tienes un objeto
grande que sí quieres conservar entero, va en el tercer parámetro
(body), que se guarda como JSON completo.
Dónde pasó: geo
geo no es un attr: es parte del sobre, como
entity o trace_id. Puede ir en el evento o en el
contexto, y las dos formas terminan igual.
estiaje.event('order.created', { geo: { lat: 25.6866, lon: -100.3161 }, // acc opcional, en metros slots: 8, });
| Caso | Resultado |
|---|---|
| En el evento y en el contexto | manda el del evento |
lat/lon fuera de rango, o basura | no sube, se conserva como geo_invalido |
0,0 | se descarta: es un GPS sin fijar, no el Golfo de Guinea |
Sin geo | el evento existe igual, solo que no sale en el mapa |
Solo lo que ocurre en un lugar real lo necesita: una visita, una venta, una tarea cerrada en sitio. Lo que pasa dentro del servidor, no. El territorio nunca inventa una posición: si un evento no la trae, lo dice.
Destinos (sinks)
Esta es la idea central del SDK: la declaración es estable y dura años; el destino es configuración. Manda a Estiaje, a la consola, a cualquier backend OTLP — Sentry, Datadog, Grafana — o a los cuatro a la vez.
import { createEstiaje, sinkConsole, sinkOtlp } from '@estiaje/sdk'; createEstiaje({ service: 'mi-api', url: process.env.ESTIAJE_URL, token: process.env.ESTIAJE_TOKEN, sinks: [ 'estiaje', sinkConsole(), sinkOtlp('https://…ingest.sentry.io/api/…/otlp/v1/logs'), { name: 'mío', send: (evs) => miLogger.info(evs) }, ], })
Un sink tuyo solo necesita dos cosas:
interface EstiajeSink { name: string; send(events: EstiajeEvent[]): void | Promise<void>; immediate?: boolean; // true = sin esperar al lote (así es la consola) }
Variables de entorno
| Variable | Qué hace |
|---|---|
ESTIAJE_URL | El endpoint. Sin ella el SDK queda apagado. |
ESTIAJE_TOKEN | Tu llave ck_live_… |
ESTIAJE_SINKS | estiaje,console,otlp — separados por coma |
OTLP_ENDPOINT | El endpoint OTLP del proveedor |
OTLP_HEADERS | Cabeceras extra, k=v,k=v |
ESTIAJE_ECHO | 0 apaga la copia a consola (viene encendida) |
La copia a consola está encendida a propósito: se ve en desarrollo, y en
AWS CloudWatch la captura sin configurar nada. Se apaga con
ESTIAJE_ECHO=0 o echo: false.
Expectativas
Un error se ve. Un proceso que se quedó a la mitad no hace ruido. Las expectativas son la forma de enterarte del silencio sin escribir una línea de código extra.
Se declaran donde vive la operación, no en el código:
{
"id": "pedido-completo",
"name": "Pedido creado → pedido completado",
"when": "order.created",
"expects": "order.completed",
"fails": ["sales.rejected", "etl.failed"],
"match_by": "attrs.order_id",
"window_min": 30,
"severity": "error"
}
Cada abridor termina en uno de tres finales, y solo uno se anuncia:
| Final | Qué pasa |
|---|---|
Cumplida — llegó lo de expects | se cierra en silencio |
Fallida declarada — llegó algo de fails | se cierra y se cuenta aparte: tu código detectó el problema y lo dijo, eso vale oro |
| Sin cierre — no llegó nada | se anuncia como expectation.unmet |
Y si tu código detecta que el proceso se truncó, dilo:
estiaje.outcome('task.canceled', { task_id }). Un final malo
declarado es información; lo que no sirve es el silencio.
Retención y cuotas
Los log son ruido y se borran según tu plan. Los
event y outcome se conservan, porque son los que
contestan preguntas de negocio meses después. Además hay un
rollup por minuto que sobrevive al borrado: el volumen y la salud de
cada servicio siguen ahí aunque el detalle ya no esté.
La cuota se cobra antes de leer el cuerpo de la petición — un lote enorme
no te cuesta CPU si ya te pasaste. Si excedes, la puerta responde
429 con Retry-After, y el SDK reintenta solo.
API HTTP
El SDK es comodidad, no obligación. Todo se puede hacer con
curl. La credencial va en Authorization: Bearer, y
la organización sale siempre de la credencial — nunca del payload ni de
un parámetro.
Mandar eventos
curl -X POST https://tu-estiaje/v1/events \ -H 'content-type: application/json' \ -H 'authorization: Bearer ck_live_…' \ -d '[{"service":"mi-api","kind":"event","name":"order.created", "attrs":{"order_id":"AND-8821"}}]'
Acepta un objeto o un arreglo. Responde 202 con
{accepted, deduped, rejected[]}: un evento malo no tumba el
lote — se rechaza solo él, con su índice y su razón.
Consultar
| Endpoint | Para qué |
|---|---|
GET /v1/events | Buscar. Filtra por q (prefijo del nombre), service, kind, severity, entity, trace, attr=clave:valor, con_geo=1. Rangos relativos: desde=-24h. Paginado por cursor. |
GET /v1/traces/:trace | Un viaje leído como historia: los pasos en orden, cuánto tardó entre uno y otro, y cuál fue el salto más largo. |
GET /v1/services | Quién está vivo: volumen, mezcla, errores y silencio por servicio. |
GET /v1/latency | p50/p95/max de attrs.ms agrupado por cualquier etiqueta o columna. name=* = todo lo que traiga ms. |
GET /v1/expectations | Qué se está vigilando y cómo va cada una. |
GET /v1/live | SSE: el río en vivo, un resumen por segundo. |
Dos tipos de credencial
| Tipo | Quién | Puede |
|---|---|---|
ck_live_… | un servicio | emitir y consultar. Se guarda cifrada y se muestra una sola vez. |
cs_… | una persona | entrar al panel. Dura 30 días. No puede emitir. |
Qué NO hacer
- No crear un cliente por request o por archivo. Uno por proceso.
- No envolver
estiaje.*en try/catch. El SDK jamás lanza. - No hacer
awaitdelog/event/outcome: son síncronos y fire-and-forget. Lo único que se espera esflush(), al apagar el proceso y al final de una lambda. - No escribir
spaceen cada evento: viene de la credencial. - No inventar plazos ni "abrir/cerrar flujos" en el código. Los plazos los define la operación en las expectativas.
log para lo
que pasó, event para lo que le importa al negocio,
outcome para lo que termina un proceso. En los eventos de
negocio incluye el identificador de la cosa. Y si tu código detecta que algo se
truncó, dilo. Eso es todo.