← SCRAM AI Lab
Instrumentar pipelines LLM multi-tier con spans nested: router → tier-selection → provider-call → parsing → side-effects. Atributos gen_ai semantic conventions y flame chart en Tempo.
May 21, 2026
13 lecturas
Un chatbot LLM en producción es un sistema distribuido escondido: el usuario manda un mensaje, tu router decide el tier, llama a un provider (Anthropic, OpenAI, Google), parsea la respuesta, ejecuta side-effects (escribir al CRM, llamar a GLPI, sincronizar con Mautic), y devuelve. Si solo tienes logs, vas a poder reconstruir el camino — pero no vas a poder ver de un vistazo dónde se fueron los 6 segundos en un request lento. Para eso existe OpenTelemetry, y para LLMs las semantic conventions gen_ai son el estándar.
POST /v1/chat/sessions/:id/messages [root span: 4.2s]
├── sanitize_message [12ms]
├── classify_intent [180ms]
├── ai_router.select_tier [3ms]
├── llm_call (tier=2, anthropic) [2.8s]
│ ├── prompt_assembly [45ms]
│ ├── anthropic.messages.create [2.7s]
│ └── response_parsing [38ms]
├── side_effects [1.1s]
│ ├── crm.capture_lead [320ms]
│ ├── glpi.create_ticket [580ms]
│ └── mautic.sync_contact [200ms]
└── persist_message [85ms]
Esa visualización en Grafana Tempo te dice en 3 segundos: "el side-effect de GLPI tardó 580ms y bloquea el response al usuario". Decisión: paralelizar side-effects no críticos, mover GLPI a background queue.
// tracing-init.ts (debe ser el PRIMER import en main.ts)
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'chat-api',
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.VERSION,
}),
traceExporter: new OTLPTraceExporter({
url: 'http://tempo:4318/v1/traces',
}),
instrumentations: [getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': { enabled: false },
})],
});
sdk.start();
import { trace, SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('chat-api');
async function callLLM(provider: string, model: string, messages: Message[]) {
return tracer.startActiveSpan('llm_call', {
attributes: {
'gen_ai.system': provider, // anthropic | openai | google
'gen_ai.request.model': model, // claude-opus-4-7
'gen_ai.request.max_tokens': 4096,
'gen_ai.request.temperature': 0.7,
},
}, async (span) => {
try {
const response = await providers[provider].call(model, messages);
span.setAttributes({
'gen_ai.response.model': response.model,
'gen_ai.response.id': response.id,
'gen_ai.usage.input_tokens': response.usage.input_tokens,
'gen_ai.usage.output_tokens': response.usage.output_tokens,
'gen_ai.response.finish_reasons': response.stop_reason,
});
return response;
} catch (err) {
span.recordException(err);
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
throw err;
} finally {
span.end();
}
});
}
Con logs estructurados puedes calcular latencia total de un request. No puedes ver:
Logs y traces son complementarios. Loki para "qué pasó a lo largo del tiempo", Tempo para "qué pasó en este request específico". Linkéalos con trace_id en el JSON payload de tus logs.
Tempo soporta tail-based sampling — recolectas todo y decides al final del trace si lo persistes. Costo de storage baja 70% sin perder los traces que importan.
La pregunta de la siguiente iteración: ¿cómo correlacionas un trace con el embedding del prompt para detectar que ciertos topics consistentemente disparan latencia alta? Estamos probando un sidecar que extrae features del prompt y los manda como atributos del span.
Artículos relacionados
Traefik v2.10 con auto-renewal certs para 94 containers
Wildcard *.scram2k.com cubre la mayoría, certs individuales para el resto. acme.json shared, DNS-01 para wildcards, HTTP-01 para subdomains. Anti-patrón: cert por container.
Loki + Grafana para logs de chatbot: query patterns
Labels útiles (org_id, session_id, tier, provider) sin caer en high-cardinality. LogQL patterns para errores por tier, p99 latency y costos por hora. Retention 30d hot.
Docker soft limits en GCP: por qué nunca hard limits
Regla operativa sobre 94 contenedores en e2-standard-4: mem_limit prohibido, solo reservations. Hard limits causan OOMKilled en bursts; reservations garantizan piso.