← SCRAM AI Lab

Tutoriales

OpenTelemetry tracing para pipelines LLM

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

Los logs te dicen qué pasó. Las traces te dicen por qué tardó 8 segundos.

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.

La jerarquía de spans en un request de chatbot

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.

Setup mínimo en NestJS

// 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();

Spans manuales con gen_ai semantic conventions

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();
    }
  });
}

Por qué structured logging no basta

Con logs estructurados puedes calcular latencia total de un request. No puedes ver:

  • El waterfall de operaciones nested (qué bloquea a qué)
  • Operaciones paralelas (¿se ejecutaron realmente en paralelo?)
  • Latencia de servicios externos a granularidad de span
  • El span específico donde cayó el error en un pipeline de 8 pasos

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.

Sampling para que no te quiebres

  • 100% de requests con error (siempre)
  • 100% de requests con latencia > p99 (tail sampling)
  • 10% sampling de requests exitosos rápidos

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.

opentelemetry
tempo
observability
← Volver a SCRAM AI Lab