Documentación de Estiaje
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 servicio no es de Node? Mira ¿Funciona con tu stack?, justo abajo.
¿Funciona con tu stack?
Con Node usas el SDK oficial. Con cualquier otro lenguaje es el mismo
POST a /v1/events, y abajo está la receta de cada uno,
lista para copiar: dónde va el código, cómo no frenar tu aplicación y cómo seguir
el hilo de un pedido de un servicio a otro.
| Stack | Cómo | Eventos y procesos vigilados | Despliegues | El viaje entre servicios | Probado |
|---|---|---|---|---|---|
| Node.js · Express, NestJS, Fastify, Next.js | SDK oficial | Sí | Solo al arrancar, o desde el CI | Sí, con el contexto | Probado en CI |
| Bun · Deno | SDK oficial | Sí | Desde el CI | Sí, con el contexto | Por probar |
| Python · Django, Flask, FastAPI | Receta HTTP | Sí | Desde el CI | Automático con el middleware | Probado en CI |
| PHP · Laravel, WordPress | Receta HTTP | Sí | Desde el CI | Automático con el middleware | Probado en CI |
| Java · Spring Boot | Receta HTTP | Sí | Desde el CI | Automático con el filtro | Probado en CI |
| C# · ASP.NET Core | Receta HTTP | Sí | Desde el CI | Automático con el middleware | Probado en CI |
| Go · net/http, Gin | Receta HTTP | Sí | Desde el CI | Automático con el middleware | Probado en CI |
| Ruby · Rails, Sinatra | Receta HTTP | Sí | Desde el CI | Automático con el middleware | Probado en CI |
| Serverless · Vercel, AWS Lambda, Cloudflare Workers | SDK o HTTP, esperando al final | Sí | Desde el CI | Pasando traceparent | Por probar |
| Sin código · n8n, Make, Zapier | Petición HTTP | Sí | — | — | Por probar |
| CI · GitHub Actions, GitLab, Bitbucket, CircleCI, Vercel | npx @estiaje/sdk desplegado | — | Sí, con quién y qué | — | Probado en CI |
Probado en CI: una prueba automática corre la receta, tal cual está en esta
página, contra Estiaje en cada cambio: el evento llega, y el hilo entra y sale con
el mismo trace_id. Las líneas propias de cada framework (registrar el
middleware) no se corren. Por probar: debería funcionar, pero todavía no lo
comprobamos.
Lo que hace cada receta
- Manda sin frenar tu app. El envío sale en segundo plano, con 2 segundos
de tiempo máximo. Si Estiaje no contesta, tu app ni se entera. Sin
ESTIAJE_TOKENno sale nada por la red. - Sigue el hilo solo. Al entrar una petición, el middleware lee la cabecera
estándar
traceparent(la misma de OpenTelemetry), o crea una si no viene. Todo lo que emitas en esa petición lleva sutrace_idsin escribirlo, y lo que sale a tus otros servicios llevatraceparentcon el mismo hilo. Así se dibuja el viaje de punta a punta. - Sin dependencias. Un archivo que copias a tu proyecto, más una o dos líneas en tu framework.
Node.js · Express, NestJS, Fastify, Next.js
El SDK oficial: Instalar, tu primer evento y, para el hilo entre servicios, Contexto y linaje.
Python · Django, Flask, FastAPI
Copia estiaje.py a tu proyecto. Python 3.8 o superior.
# estiaje.py: copia este archivo a tu proyecto. Sin dependencias.
import contextvars, json, os, re, secrets, threading, urllib.request
URL = (os.environ.get("ESTIAJE_URL") or "https://estiaje.mx") + "/v1/events"
SERVICIO = "mi-api"
_hilo = contextvars.ContextVar("estiaje_hilo", default=None)
_envios = []
_TP = re.compile(r"^[0-9a-f]{2}-([0-9a-f]{32})-[0-9a-f]{16}-[0-9a-f]{2}$")
def empezar_hilo(traceparent=None):
"""Al entrar una petición: toma el hilo de la cabecera o crea uno nuevo."""
m = _TP.match((traceparent or "").strip().lower())
hilo = m.group(1) if m and m.group(1) != "0" * 32 else secrets.token_hex(16)
_hilo.set(hilo)
return hilo
def traceparent():
"""Para las llamadas que salen a otro servicio: el mismo hilo, un paso nuevo."""
return "00-%s-%s-01" % (_hilo.get() or secrets.token_hex(16), secrets.token_hex(8))
def evento(name, attrs=None, kind="event"):
"""Manda el evento en segundo plano: nunca frena ni rompe tu app."""
if not os.environ.get("ESTIAJE_TOKEN"):
return # sin llave no sale nada por la red
ev = {"service": SERVICIO, "kind": kind, "name": name, "attrs": attrs or {}}
if _hilo.get():
ev["trace_id"] = _hilo.get()
envio = threading.Thread(target=_mandar, args=([ev],))
envio.start()
_envios[:] = [e for e in _envios if e.is_alive()] + [envio]
def esperar():
"""En una lambda, antes de devolver: que salga lo pendiente (2 s como máximo)."""
for e in list(_envios):
e.join()
def _mandar(lote):
try:
req = urllib.request.Request(URL, data=json.dumps(lote).encode(), headers={
"content-type": "application/json",
"authorization": "Bearer " + os.environ["ESTIAJE_TOKEN"]})
urllib.request.urlopen(req, timeout=2).close()
except Exception:
pass # si Estiaje no contesta, tu app sigue
Donde pase algo:
import estiaje
estiaje.evento("pedido.creado", {"order_id": pedido.id, "monto": pedido.total})
Django: el hilo, en un middleware.
# miapp/middleware.py (y en settings.MIDDLEWARE: "miapp.middleware.hilo_estiaje")
import estiaje
def hilo_estiaje(get_response):
def middleware(request):
estiaje.empezar_hilo(request.headers.get("traceparent"))
return get_response(request)
return middleware
Flask:
@app.before_request
def hilo_estiaje():
estiaje.empezar_hilo(request.headers.get("traceparent"))
FastAPI: el hilo en un middleware, y el evento con BackgroundTasks
para que salga después de responder.
from fastapi import BackgroundTasks, FastAPI, Request
import estiaje
app = FastAPI()
@app.middleware("http")
async def hilo_estiaje(request: Request, call_next):
estiaje.empezar_hilo(request.headers.get("traceparent"))
return await call_next(request)
@app.post("/pedidos")
async def crear(p: Pedido, tareas: BackgroundTasks):
...
tareas.add_task(estiaje.evento, "pedido.creado", {"order_id": p.id, "monto": p.total})
Al llamar a otro de tus servicios, pasa el hilo:
requests.get(url, headers={"traceparent": estiaje.traceparent()}, timeout=5)
En un script o un cron no hace falta nada más: Python espera el envío antes de salir.
PHP · Laravel, WordPress
Copia estiaje.php a tu proyecto. PHP 7.4 o superior, con la extensión
curl. Los eventos se juntan y se mandan al final, cuando tu usuario ya
tiene su respuesta (con PHP-FPM).
<?php
// estiaje.php: cópialo a tu proyecto (en Laravel, app/Estiaje.php). Sin dependencias.
final class Estiaje
{
const SERVICIO = 'mi-api';
private static ?string $hilo = null;
private static array $lote = [];
/** Al entrar una petición: toma el hilo de la cabecera o crea uno nuevo. */
public static function empezarHilo(?string $traceparent): string
{
$tp = strtolower(trim((string) $traceparent));
$ok = preg_match('/^[0-9a-f]{2}-([0-9a-f]{32})-[0-9a-f]{16}-[0-9a-f]{2}$/', $tp, $m);
self::$hilo = $ok && $m[1] !== str_repeat('0', 32) ? $m[1] : bin2hex(random_bytes(16));
return self::$hilo;
}
/** Para las llamadas que salen a otro servicio: el mismo hilo, un paso nuevo. */
public static function traceparent(): string
{
return '00-' . (self::$hilo ?? bin2hex(random_bytes(16))) . '-' . bin2hex(random_bytes(8)) . '-01';
}
/** Junta el evento; se manda al final, ya que tu usuario tiene su respuesta. */
public static function evento(string $name, array $attrs = [], string $kind = 'event'): void
{
if (!getenv('ESTIAJE_TOKEN')) return; // sin llave no sale nada por la red
$ev = ['service' => self::SERVICIO, 'kind' => $kind, 'name' => $name, 'attrs' => (object) $attrs];
if (self::$hilo) $ev['trace_id'] = self::$hilo;
if (!self::$lote) register_shutdown_function([self::class, 'mandar']);
self::$lote[] = $ev;
}
public static function mandar(): void
{
if (function_exists('fastcgi_finish_request')) fastcgi_finish_request();
$lote = self::$lote;
self::$lote = [];
if (!$lote) return;
$ch = curl_init((getenv('ESTIAJE_URL') ?: 'https://estiaje.mx') . '/v1/events');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['content-type: application/json',
'authorization: Bearer ' . getenv('ESTIAJE_TOKEN')],
CURLOPT_POSTFIELDS => json_encode($lote),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT_MS => 1000,
CURLOPT_TIMEOUT_MS => 2000,
]);
curl_exec($ch); // si Estiaje no contesta, tu app sigue
}
}
Donde pase algo:
Estiaje::evento('pedido.creado', ['order_id' => $pedido->id, 'monto' => $pedido->total]);
Laravel: el hilo, en un middleware.
// app/Http/Middleware/HiloEstiaje.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class HiloEstiaje
{
public function handle(Request $request, Closure $next)
{
\Estiaje::empezarHilo($request->header('traceparent'));
return $next($request);
}
}
// bootstrap/app.php (Laravel 11+; antes, en $middleware de app/Http/Kernel.php)
->withMiddleware(fn ($middleware) => $middleware->append(\App\Http\Middleware\HiloEstiaje::class))
Al llamar a otro de tus servicios con el cliente de Laravel, pasa el hilo:
Http::withHeaders(['traceparent' => \Estiaje::traceparent()])->get($url);
¿Prefieres mandar el evento con el cliente de Laravel? Que salga después de responder:
// en config/services.php: 'estiaje' => ['token' => env('ESTIAJE_TOKEN')]
dispatch(fn () => Http::timeout(2)->withToken(config('services.estiaje.token'))
->post('https://estiaje.mx/v1/events', [[
'service' => 'mi-api', 'kind' => 'event', 'name' => 'pedido.creado',
'attrs' => ['order_id' => $pedido->id, 'monto' => $pedido->total],
]]))->afterResponse();
estiaje.php lee la llave con getenv. Con
php artisan config:cache, Laravel ya no carga el .env:
pon ESTIAJE_TOKEN como variable del servidor.
WordPress, en tu plugin o en functions.php:
require_once __DIR__ . '/estiaje.php';
add_action('init', fn () => Estiaje::empezarHilo($_SERVER['HTTP_TRACEPARENT'] ?? null));
// al llamar a otro servicio
wp_remote_get($url, ['headers' => ['traceparent' => Estiaje::traceparent()]]);
Java · Spring Boot
Copia Estiaje.java a tu proyecto. Java 17 o superior.
// Estiaje.java: cópialo a tu proyecto (con tu package). Sin dependencias, Java 17+.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class Estiaje {
static final String SERVICIO = "mi-api";
static final String TOKEN = System.getenv("ESTIAJE_TOKEN");
static final String BASE = System.getenv("ESTIAJE_URL");
static final String URL = (BASE == null || BASE.isEmpty() ? "https://estiaje.mx" : BASE) + "/v1/events";
static final HttpClient HTTP = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(1)).build();
static final ThreadLocal<String> HILO = new ThreadLocal<>();
static final Pattern TP = Pattern.compile("^[0-9a-f]{2}-([0-9a-f]{32})-[0-9a-f]{16}-[0-9a-f]{2}$");
static final SecureRandom AZAR = new SecureRandom();
/** Al entrar una petición: toma el hilo de la cabecera o crea uno nuevo. */
public static String empezarHilo(String traceparent) {
Matcher m = TP.matcher(traceparent == null ? "" : traceparent.trim().toLowerCase());
String hilo = m.matches() && !m.group(1).equals("0".repeat(32)) ? m.group(1) : hex(16);
HILO.set(hilo);
return hilo;
}
/** Al terminar la petición: el hilo de Java se reusa para la siguiente. */
public static void terminarHilo() {
HILO.remove();
}
/** Para las llamadas que salen a otro servicio: el mismo hilo, un paso nuevo. */
public static String traceparent() {
String hilo = HILO.get();
return "00-" + (hilo != null ? hilo : hex(16)) + "-" + hex(8) + "-01";
}
/** Manda el evento en segundo plano: nunca frena ni rompe tu app. */
public static CompletableFuture<Void> evento(String name, Map<String, ?> attrs) {
try {
if (TOKEN == null || TOKEN.isEmpty()) return CompletableFuture.completedFuture(null);
StringBuilder j = new StringBuilder("[{\"service\":" + q(SERVICIO) + ",\"kind\":\"event\",\"name\":" + q(name));
if (HILO.get() != null) j.append(",\"trace_id\":").append(q(HILO.get()));
j.append(",\"attrs\":{");
String coma = "";
for (Map.Entry<String, ?> a : attrs.entrySet()) {
Object v = a.getValue();
j.append(coma).append(q(a.getKey())).append(':')
.append(v instanceof Number || v instanceof Boolean ? v.toString() : q(String.valueOf(v)));
coma = ",";
}
j.append("}}]");
HttpRequest req = HttpRequest.newBuilder(URI.create(URL)).timeout(Duration.ofSeconds(2))
.header("content-type", "application/json")
.header("authorization", "Bearer " + TOKEN)
.POST(HttpRequest.BodyPublishers.ofString(j.toString())).build();
return HTTP.sendAsync(req, HttpResponse.BodyHandlers.discarding()).handle((r, e) -> null);
} catch (RuntimeException e) {
return CompletableFuture.completedFuture(null); // si algo falla, tu app sigue
}
}
static String q(String s) {
StringBuilder b = new StringBuilder("\"");
for (char c : s.toCharArray()) {
if (c == '"' || c == '\\') b.append('\\').append(c);
else if (c < 0x20) b.append(String.format("\\u%04x", (int) c));
else b.append(c);
}
return b.append('"').toString();
}
static String hex(int bytes) {
byte[] b = new byte[bytes];
AZAR.nextBytes(b);
StringBuilder s = new StringBuilder();
for (byte x : b) s.append(String.format("%02x", x));
return s.toString();
}
}
Donde pase algo:
Estiaje.evento("pedido.creado", Map.of("order_id", pedido.getId(), "monto", pedido.getTotal()));
Spring Boot: el hilo, en un filtro.
@Component
public class HiloEstiaje extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws ServletException, IOException {
Estiaje.empezarHilo(req.getHeader("traceparent"));
try {
chain.doFilter(req, res);
} finally {
Estiaje.terminarHilo();
}
}
}
Al llamar a otro de tus servicios (con RestClient), pasa el hilo:
RestClient http = RestClient.builder()
.requestInterceptor((req, cuerpo, sigue) -> {
req.getHeaders().set("traceparent", Estiaje.traceparent());
return sigue.execute(req, cuerpo);
})
.build();
En un programa que termina enseguida, espera el envío:
Estiaje.evento(…).join().
C# · ASP.NET Core
Copia Estiaje.cs a tu proyecto. .NET 6 o superior.
// Estiaje.cs: cópialo a tu proyecto. Sin paquetes, .NET 6 o superior.
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
public static class Estiaje
{
const string Servicio = "mi-api";
static readonly HttpClient Http = new() { Timeout = TimeSpan.FromSeconds(2) };
static readonly string Base = Environment.GetEnvironmentVariable("ESTIAJE_URL") ?? "";
static readonly string Url = (Base.Length > 0 ? Base : "https://estiaje.mx") + "/v1/events";
static readonly AsyncLocal<string?> Hilo = new();
static readonly Regex Formato = new("^[0-9a-f]{2}-([0-9a-f]{32})-[0-9a-f]{16}-[0-9a-f]{2}$");
/// Al entrar una petición: toma el hilo de la cabecera o crea uno nuevo.
public static string EmpezarHilo(string? traceparent)
{
var m = Formato.Match((traceparent ?? "").Trim().ToLowerInvariant());
Hilo.Value = m.Success && m.Groups[1].Value != new string('0', 32) ? m.Groups[1].Value : Azar(16);
return Hilo.Value;
}
/// Para las llamadas que salen a otro servicio: el mismo hilo, un paso nuevo.
public static string Traceparent() => $"00-{Hilo.Value ?? Azar(16)}-{Azar(8)}-01";
/// Manda el evento en segundo plano: nunca frena ni rompe tu app. No hace falta esperarlo.
public static Task Evento(string name, object? attrs = null)
{
var token = Environment.GetEnvironmentVariable("ESTIAJE_TOKEN");
if (string.IsNullOrEmpty(token)) return Task.CompletedTask; // sin llave no sale nada
var ev = new Dictionary<string, object?>
{
["service"] = Servicio, ["kind"] = "event", ["name"] = name, ["attrs"] = attrs ?? new { },
};
if (Hilo.Value is { } hilo) ev["trace_id"] = hilo;
var req = new HttpRequestMessage(HttpMethod.Post, Url) { Content = JsonContent.Create(new[] { ev }) };
req.Headers.TryAddWithoutValidation("authorization", "Bearer " + token);
return Http.SendAsync(req).ContinueWith(t => // si Estiaje no contesta, tu app sigue
{
if (t.IsCompletedSuccessfully) t.Result.Dispose(); else _ = t.Exception;
});
}
static string Azar(int bytes) => Convert.ToHexString(RandomNumberGenerator.GetBytes(bytes)).ToLowerInvariant();
}
/// Pone traceparent en lo que sale por un HttpClient hacia tus otros servicios.
public class EstiajeSaliente : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage req, CancellationToken ct)
{
req.Headers.Remove("traceparent");
req.Headers.TryAddWithoutValidation("traceparent", Estiaje.Traceparent());
return base.SendAsync(req, ct);
}
}
En Program.cs: el hilo en un middleware, y que tus HttpClient
lo pasen a tus otros servicios.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTransient<EstiajeSaliente>();
builder.Services.AddHttpClient("inventario").AddHttpMessageHandler<EstiajeSaliente>();
var app = builder.Build();
app.Use(async (ctx, next) =>
{
Estiaje.EmpezarHilo(ctx.Request.Headers["traceparent"]);
await next();
});
Donde pase algo (no hace falta await):
Estiaje.Evento("pedido.creado", new { order_id = pedido.Id, monto = pedido.Total });
En una app de consola que termina enseguida, sí espéralo:
await Estiaje.Evento(…).
Go · net/http, Gin
Copia estiaje.go a tu proyecto, en su paquete estiaje. Go 1.18
o superior.
// estiaje.go: cópialo a tu proyecto (paquete estiaje). Sin dependencias.
package estiaje
import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"net/http"
"os"
"regexp"
"strings"
"sync"
"time"
)
const servicio = "mi-api"
var (
cliente = &http.Client{Timeout: 2 * time.Second}
formato = regexp.MustCompile(`^[0-9a-f]{2}-([0-9a-f]{32})-[0-9a-f]{16}-[0-9a-f]{2}$`)
pendientes sync.WaitGroup
)
type claveHilo struct{}
// ConHilo: al entrar una petición, toma el hilo de la cabecera o crea uno nuevo.
func ConHilo(ctx context.Context, traceparent string) context.Context {
hilo := azar(16)
if m := formato.FindStringSubmatch(strings.ToLower(strings.TrimSpace(traceparent))); m != nil && m[1] != strings.Repeat("0", 32) {
hilo = m[1]
}
return context.WithValue(ctx, claveHilo{}, hilo)
}
// Middleware para net/http (sirve igual con chi o gorilla/mux).
func Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r.WithContext(ConHilo(r.Context(), r.Header.Get("traceparent"))))
})
}
func Hilo(ctx context.Context) string {
h, _ := ctx.Value(claveHilo{}).(string)
return h
}
// Traceparent: para las llamadas que salen a otro servicio. El mismo hilo, un paso nuevo.
func Traceparent(ctx context.Context) string {
h := Hilo(ctx)
if h == "" {
h = azar(16)
}
return "00-" + h + "-" + azar(8) + "-01"
}
// Transporte pone traceparent en todo lo que sale por un http.Client:
// &http.Client{Transport: estiaje.Transporte{}}, y la petición con su ctx.
type Transporte struct{ Base http.RoundTripper }
func (t Transporte) RoundTrip(r *http.Request) (*http.Response, error) {
r = r.Clone(r.Context())
r.Header.Set("traceparent", Traceparent(r.Context()))
if t.Base == nil {
return http.DefaultTransport.RoundTrip(r)
}
return t.Base.RoundTrip(r)
}
// Evento lo manda en segundo plano: nunca frena ni rompe tu app.
func Evento(ctx context.Context, name string, attrs map[string]any) {
token := os.Getenv("ESTIAJE_TOKEN")
if token == "" {
return // sin llave no sale nada por la red
}
if attrs == nil {
attrs = map[string]any{}
}
ev := map[string]any{"service": servicio, "kind": "event", "name": name, "attrs": attrs}
if h := Hilo(ctx); h != "" {
ev["trace_id"] = h
}
cuerpo, err := json.Marshal([]any{ev})
if err != nil {
return
}
url := os.Getenv("ESTIAJE_URL")
if url == "" {
url = "https://estiaje.mx"
}
pendientes.Add(1)
go func() {
defer pendientes.Done()
req, err := http.NewRequest("POST", url+"/v1/events", bytes.NewReader(cuerpo))
if err != nil {
return
}
req.Header.Set("content-type", "application/json")
req.Header.Set("authorization", "Bearer "+token)
if res, err := cliente.Do(req); err == nil {
res.Body.Close()
}
}()
}
// Esperar: en un programa que termina enseguida (un cron), llámalo antes de salir.
func Esperar() { pendientes.Wait() }
func azar(n int) string {
b := make([]byte, n)
rand.Read(b)
return hex.EncodeToString(b)
}
net/http (y chi, gorilla/mux): envuelve tu router, y usa el
Transporte para lo que sale.
http.ListenAndServe(":8080", estiaje.Middleware(mux))
// donde pase algo, con el contexto de la petición
estiaje.Evento(r.Context(), "pedido.creado", map[string]any{"order_id": p.ID, "monto": p.Total})
// al llamar a otro de tus servicios
cliente := &http.Client{Transport: estiaje.Transporte{}}
req, _ := http.NewRequestWithContext(r.Context(), "GET", url, nil)
cliente.Do(req)
Gin:
r.Use(func(c *gin.Context) {
c.Request = c.Request.WithContext(estiaje.ConHilo(c.Request.Context(), c.GetHeader("traceparent")))
c.Next()
})
En un programa que termina enseguida (un cron), llama estiaje.Esperar()
antes de salir.
Ruby · Rails, Sinatra
Copia estiaje.rb a tu proyecto. Ruby 2.7 o superior, sin gemas.
# estiaje.rb: cópialo a tu proyecto (en Rails, lib/estiaje.rb). Sin gemas.
require "json"
require "net/http"
require "securerandom"
module Estiaje
SERVICIO = "mi-api"
FORMATO = /\A[0-9a-f]{2}-([0-9a-f]{32})-[0-9a-f]{16}-[0-9a-f]{2}\z/
@envios = []
@candado = Mutex.new
# Al entrar una petición: toma el hilo de la cabecera o crea uno nuevo.
def self.empezar_hilo(traceparent)
m = FORMATO.match(traceparent.to_s.strip.downcase)
Thread.current[:estiaje_hilo] = m && m[1] != "0" * 32 ? m[1] : SecureRandom.hex(16)
end
def self.hilo
Thread.current[:estiaje_hilo]
end
# Para las llamadas que salen a otro servicio: el mismo hilo, un paso nuevo.
def self.traceparent
"00-#{hilo || SecureRandom.hex(16)}-#{SecureRandom.hex(8)}-01"
end
# Manda el evento en segundo plano: nunca frena ni rompe tu app.
def self.evento(name, attrs = {}, kind: "event")
token = ENV["ESTIAJE_TOKEN"].to_s
return if token.empty? # sin llave no sale nada por la red
ev = { service: SERVICIO, kind: kind, name: name, attrs: attrs }
ev[:trace_id] = hilo if hilo
base = ENV["ESTIAJE_URL"].to_s.empty? ? "https://estiaje.mx" : ENV["ESTIAJE_URL"]
envio = Thread.new do
uri = URI("#{base}/v1/events")
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
open_timeout: 1, read_timeout: 2, write_timeout: 2) do |http|
http.post(uri.path, [ev].to_json,
"content-type" => "application/json", "authorization" => "Bearer #{token}")
end
rescue StandardError
nil # si Estiaje no contesta, tu app sigue
end
@candado.synchronize { @envios.select!(&:alive?); @envios << envio }
end
# En un script que termina enseguida (un cron, un rake), llámalo antes de salir.
def self.esperar
@candado.synchronize { @envios.dup }.each(&:join)
end
# El middleware de Rack: Rails, Sinatra, Hanami.
class Middleware
def initialize(app)
@app = app
end
def call(env)
Estiaje.empezar_hilo(env["HTTP_TRACEPARENT"])
@app.call(env)
ensure
Thread.current[:estiaje_hilo] = nil
end
end
end
Rails, en config/application.rb (en Sinatra:
use Estiaje::Middleware):
require_relative "../lib/estiaje" # …dentro de class Application: config.middleware.use Estiaje::Middleware
Donde pase algo, y al llamar a otro de tus servicios:
Estiaje.evento("pedido.creado", { order_id: pedido.id, monto: pedido.total })
req = Net::HTTP::Get.new(uri)
req["traceparent"] = Estiaje.traceparent
En un script o una tarea de rake, llama Estiaje.esperar antes de salir.
Serverless · Vercel, AWS Lambda, Cloudflare Workers
Una función serverless se congela en cuanto devuelve su respuesta, y lo que estaba por salir se queda sin salir. Por eso, aquí sí se espera el envío al final.
- Vercel y Lambda con Node:
await estiaje.flush()antes de devolver la respuesta. - Lambda con las recetas de arriba:
estiaje.esperar()en Python,estiaje.Esperar()en Go,Estiaje.esperaren Ruby,.join()en Java yawaiten C#. - Cloudflare Workers:
ctx.waitUntildeja que el envío termine después de responder, sin hacer esperar a tu usuario.
export default {
async fetch(request, env, ctx) {
// el hilo: el de la cabecera traceparent, o uno nuevo
const m = /^[0-9a-f]{2}-([0-9a-f]{32})-/.exec(request.headers.get('traceparent') ?? '');
const trace_id = m ? m[1] : crypto.randomUUID().replaceAll('-', '');
ctx.waitUntil(fetch('https://estiaje.mx/v1/events', {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${env.ESTIAJE_TOKEN}` },
body: JSON.stringify([{ service: 'mi-worker', kind: 'event', name: 'pedido.creado', trace_id }]),
}).catch(() => {})); // si Estiaje no contesta, tu worker sigue
return new Response('ok');
},
};
Sin código · n8n, Make, Zapier
Un paso de petición HTTP al final de tu flujo: en n8n, el nodo HTTP Request; en Make, HTTP › Make a request; en Zapier, Webhooks by Zapier › Custom Request.
| Campo | Valor |
|---|---|
| Método | POST |
| URL | https://estiaje.mx/v1/events |
| Cabeceras | authorization: Bearer ck_live_… y content-type: application/json |
| Cuerpo (JSON) | el de abajo (así se escribe en n8n), con los datos del paso anterior |
[{"service": "n8n", "kind": "event", "name": "pedido.creado",
"attrs": {"order_id": "{{ $json.id }}", "monto": {{ $json.total }}}}]
La llave guárdala en las credenciales de la herramienta, no escrita en el paso. Marca el paso para que siga aunque falle: si Estiaje no contesta, tu flujo no se detiene.
CI · GitHub Actions, GitLab, Bitbucket, CircleCI, Vercel
Una línea al final de tu pipeline avisa de cada despliegue, con quién y qué:
npx @estiaje/sdk desplegado. Funciona sin importar en qué lenguaje
esté tu servicio. Los detalles, en Despliegues.
¿Quieres un SDK para tu lenguaje, con cola en memoria y reintentos como el de Node? Escríbenos a hola@estiaje.mx: el primero que lo pida decide cuál sigue.
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_TOKEN=ck_live_…
Una sola línea: con la llave, el SDK ya manda a https://estiaje.mx.
Sin ESTIAJE_TOKEN no sale nada por la red — todas las funciones
existen e imprimen a consola. Puedes instrumentar hoy y poner la llave 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.
Tus logs de siempre
Si tu app ya escribe con console.log, pino o winston, no cambies
nada: una línea y esos logs llegan a Estiaje, donde los buscas por texto y los
filtras por servicio, severidad o cualquier etiqueta. Vienen en todos los
planes, desde Gratis. Desde el SDK 0.7.0.
// console.log, info, warn y error, tal como ya los escribes
estiaje.capturarConsola();
// pino: a la consola y a Estiaje
const logger = pino(pino.multistream([
{ stream: process.stdout },
{ stream: estiaje.destino() },
]));
// winston: un transporte más
logger.add(new winston.transports.Stream({ stream: estiaje.destino() }));
- Tu consola imprime igual que antes. Estiaje solo escucha.
- La severidad se respeta:
console.erroro un nivelerrorde pino llegan como error, con su stack. - Las etiquetas de pino y winston (
{ pedido: 'A-1042' }) llegan como etiquetas, y se pueden filtrar. - Un tope de 20 líneas por segundo (
{ porSegundo }para cambiarlo): un ciclo que imprime mil no se come la cola, y las que no entran se cuentan. Si la cola se llena, lo primero que se tira son logs, nunca un evento o un desenlace. console.debugno se manda de fábrica:capturarConsola({ metodos: ['debug', 'log', 'info', 'warn', 'error'] }).
Los logs se guardan 3 días en Gratis, 7 en Inicio, 30 en Pro y 90 en Max. Los
errores, más: 14 días, 90 días o un año (ver Retención y
cuotas). Desde otro lenguaje, manda kind: "log" por la
API HTTP.
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 || 'https://estiaje.mx', 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_TOKEN | Tu llave ck_live_…. Es la única que hace falta. Sin ella no sale nada por la red. |
ESTIAJE_URL | Opcional. Otro destino (desarrollo local, tu propio servidor). Sin ella: https://estiaje.mx |
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) |
ESTIAJE_VERSION | Opcional. La versión que se anuncia al arrancar (ver Despliegues) |
ESTIAJE_REPO_URL | Opcional. Tu repositorio, para ligar cada versión a su commit (ver Despliegues) |
ESTIAJE_DESPLIEGUES | 0 apaga el anuncio de la versión al arrancar |
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.
Cuánto vale cada proceso
«3 sin cierre» es un número. «$1,800 en riesgo» es un hallazgo. Cada expectativa puede decir cuánto vale cada proceso que vigila, en pesos, de una de tres formas (en el panel: ¿Cuánto vale cada uno?):
// no sé / no aplica "valor": null // cada pedido vale $600 "valor": { "tipo": "fijo", "monto": 600 } // lo dice el evento que abre (attrs.monto); si no lo trae, $600 "valor": { "tipo": "atributo", "atributo": "monto", "respaldo": 600 }
El monto se lee del evento que abre (el de when). Sirven
números y textos como "1,234.50", "$600" o
"700 MXN" (la coma es de miles). Un negativo o algo que no es un
monto cuenta como «no lo trae» y vale el respaldo. No hay que tocar el código ni
emitir nada nuevo: si tu evento ya lleva monto, total o
amount, el panel lo propone.
| Cuenta | Qué es |
|---|---|
| En riesgo | lo que se pasó de su plazo y sigue sin ningún cierre, de los últimos 7 días. El dinero que todavía puedes ir a rescatar. |
| Vencido sin cierre | todo lo que se pasó de su plazo en el periodo, aunque después haya cerrado tarde: la suma de la lista «sin cierre». |
| Cerrado a tiempo | lo que recibió su cierre feliz dentro del plazo. |
Se calcula al consultar, con el valor vigente: si le pones valor hoy, ves en pesos
lo que ya estaba abierto. Y el aviso al teléfono lo dice:
«$1,800 en riesgo: Pedidos surtidos», con el nombre que le pusiste al proceso
(sin valor: «3 pedidos sin cierre: Pedidos surtidos»).
Por la API: valor al declarar o en un PATCH, y
GET /v1/orgs/:org/valor/resumen?desde=&hasta= para el resumen de
la organización.
Rescatar uno
Encontrar el pedido atorado es la mitad; la otra es abrirlo en tu sistema. Cada expectativa puede llevar la liga a tu sistema, una vez, con un hueco donde va la clave de cada proceso (en el panel: Para rescatar uno, abrir):
"liga_rescate": "https://admin.tu-tienda.mx/pedidos/{order_id}"
Cada renglón de «lo que se quedó sin cierre» que sigue abierto lleva entonces
Rescatar, que abre ese pedido. El hueco se llama como la clave con la que se
empareja el proceso ({order_id} para attrs.order_id,
{entity} para entity) o, siempre, {clave}.
La liga empieza con https://, el hueco no puede ir en el nombre del
sitio y la clave va codificada: un pedido 48/9 abre
…/pedidos/48%2F9. Por la API: liga_rescate al declarar o
en un PATCH (null la quita), y cada renglón de
GET /v1/expectations/:id/unmet trae su liga ya llena.
Despliegues
Cuando algo se rompe, la primera pregunta es «¿qué cambió?». Estiaje sabe cuándo desplegaste, qué y quién, por dos caminos que llegan al mismo lugar.
Solo, al arrancar
Desde la versión 0.5.0 del SDK, cuando tu proceso arranca mira la versión que dejó la plataforma en el entorno y la anuncia una vez. No escribes nada. Si la versión no cambió es un reinicio y no se cuenta; diez réplicas que arrancan con la misma versión son un solo despliegue.
| La versión sale de (la primera que haya) | Dónde |
|---|---|
ESTIAJE_VERSION | la tuya: le gana a todo |
VERCEL_GIT_COMMIT_SHA | Vercel, con quién, título y rama |
RAILWAY_GIT_COMMIT_SHA | Railway, con quién, título y rama |
RENDER_GIT_COMMIT | Render, con la rama |
HEROKU_SLUG_COMMIT, SOURCE_VERSION | Heroku |
K_REVISION | Cloud Run |
AWS_LAMBDA_FUNCTION_VERSION | Lambda (menos $LATEST) |
FLY_IMAGE_REF | Fly.io |
npm_package_version | tu package.json, si arrancaste con npm start |
Sin ninguna no se manda nada; tampoco sin ESTIAJE_TOKEN ni con
NODE_ENV=test. GITHUB_SHA no cuenta aquí: GitHub Actions es
el CI, no donde corre tu servicio (para eso está el paso de abajo). Se apaga con despliegues: false o
ESTIAJE_DESPLIEGUES=0.
Con quién y qué, desde el CI
Una línea al final del pipeline:
npx @estiaje/sdk desplegado
Lee ESTIAJE_TOKEN y deduce del CI quién desplegó, el título del
commit, la rama y el sha: GitHub Actions, GitLab, Bitbucket, CircleCI y Vercel,
y si no, git. En GitHub Actions:
- name: Avisar a Estiaje run: npx -y @estiaje/sdk desplegado --servicio mi-api env: ESTIAJE_TOKEN: ${{ secrets.ESTIAJE_TOKEN }} # y si algo falló antes, que también se sepa - name: Avisar a Estiaje que falló if: failure() run: npx -y @estiaje/sdk desplegado --servicio mi-api --fallo env: ESTIAJE_TOKEN: ${{ secrets.ESTIAJE_TOKEN }}
En GitLab CI, con ESTIAJE_TOKEN como variable enmascarada del proyecto:
avisar-estiaje: stage: .post script: npx -y @estiaje/sdk desplegado --servicio mi-api avisar-estiaje-fallo: stage: .post when: on_failure script: npx -y @estiaje/sdk desplegado --servicio mi-api --fallo
La liga al commit. Desde la 0.8.0, con el despliegue viaja la dirección de tu
repositorio (la del CI, la de Vercel o Railway, o el origin de git;
ESTIAJE_REPO_URL le gana). Si la versión es un sha, el panel la liga a su
commit en GitHub, GitLab o Bitbucket. Sin integraciones ni permisos: es solo la
dirección, y nunca viaja con usuario ni clave.
| Bandera | Qué hace |
|---|---|
--servicio <nombre> | El mismo de estiajeFromEnv({ service }). Sin ella: el name del package.json, o el repo |
--version <v> | Sin ella: el sha del commit |
--titulo, --quien | Si no quieres lo que dice el CI |
--fallo | El despliegue falló (para el paso con if: failure()) |
--silencioso | No imprime nada si todo sale bien |
Tu estado en tu sitio
Tu página de estado (estiaje.mx/estado/tu-negocio, desde el plan
Inicio) también puede vivir dentro de tu propio sitio, en los planes Pro y Max:
incrustada tal cual, o
leyendo sus datos para pintarlos con tu diseño. Las dos dependen de la
misma lista: los sitios que pueden mostrarla.
Publica la página en el panel, en Salud → Página de estado.
Elige qué servicios salen en Servicios en la página. Salen con su nombre; nunca direcciones ni errores.
Agrega tus sitios en Ponerla en tu sitio: escribe
tu-negocio.mx y se guarda como https://tu-negocio.mx.
Solo https, sin IPs ni comodines, hasta 10. Si tienes
www. y sin www., agrega los dos.
Incrustada
Pega esto donde la quieras ver (el panel te lo da ya con tu dirección):
<iframe src="https://estiaje.mx/incrustar/tu-negocio" title="Estado de Tu Negocio" style="width:100%;height:520px;border:0" loading="lazy"></iframe> <script>addEventListener('message',e=>{if(e.origin==='https://estiaje.mx'&&e.data&&e.data.estiaje==='alto') document.querySelector('iframe[src^="https://estiaje.mx/incrustar/"]').style.height=e.data.alto+'px'})</script>
Sale sin el encabezado de la página completa, con fondo transparente y un
«Estado por Estiaje» discreto que abre la página entera en otra pestaña. El
<script> es opcional: la página avisa su alto y el iframe se
ajusta solo, sin barra de scroll. Es clara; si tu sitio es oscuro, agrega
?tema=oscuro a la dirección (o ?tema=auto para seguir
al teléfono de quien la ve). Solo se deja meter en los sitios de tu lista; en
cualquier otro, el navegador no la muestra.
Los datos, con tu diseño
La misma información que usa la página, en JSON, sin llave:
GET https://estiaje.mx/v1/estado/tu-negocio
{
"titulo": "Tu Negocio",
"estado_general": "operando", // o "falla_parcial", "caida"
"actualizado": "2026-10-04T18:42:10.000Z",
"servicios": [{
"nombre": "Tienda en línea",
"estado": "arriba", // o "caida", "pausado"
"uptime_90d": 99.97, // null si aún no hay datos
"dias": [{ "dia": "2026-07-07", "uptime": 100, "estado": "ok" }, /* … 90 días, el más viejo primero */]
}],
"incidentes": [{
"servicio": "Tienda en línea",
"inicio": "2026-09-28T03:12:00.000Z",
"fin": "2026-09-28T03:19:00.000Z", // null = sigue caído
"duracion_min": 7
}]
}
Cada día trae estado: "ok", "falla" (fallas
breves), "caida" o "sin_datos". Si la página no está
publicada responde 404.
Desde el navegador, en una página de uno de tus sitios (en cualquier otro sitio el navegador no la deja leer):
const r = await fetch('https://estiaje.mx/v1/estado/tu-negocio'); const estado = await r.json(); document.querySelector('#estado').textContent = estado.estado_general === 'operando' ? 'Todo funciona' : 'Tenemos fallas';
Desde tu servidor (Node 18 o más) funciona sin importar la lista. Guárdala 30 segundos: la respuesta no cambia más seguido.
let guardado = null, cuando = 0; export async function estadoDeMiNegocio() { if (guardado && Date.now() - cuando < 30_000) return guardado; const r = await fetch('https://estiaje.mx/v1/estado/tu-negocio', { signal: AbortSignal.timeout(3000) }); if (!r.ok) return guardado; // lo de antes, si había guardado = await r.json(); cuando = Date.now(); return guardado; }
429 con Retry-After. Desde el navegador de tus clientes
no se nota (cada quien cuenta aparte); desde tu servidor, con la caché de 30
segundos te sobra.
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://estiaje.mx/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.
Desde tu lenguaje
El SDK oficial es de Node/TypeScript. Para lo demás no hace falta esperar uno: es
este mismo POST, mandado sin esperar la respuesta y con un
timeout corto para que Estiaje nunca frene tu aplicación. Las recetas para Python,
PHP, Java, C#, Go y Ruby, con su middleware para seguir el hilo entre servicios,
están en ¿Funciona con tu stack?.
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, con su valor y lo que está en riesgo en pesos. |
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.