---
name: tracing-processor
description: Documenta el patrón TracingProcessor para el sistema de trazabilidad SWL. Cómo crear exportadores de trazas (Langfuse, Jaeger, consola) sin modificar el núcleo, usando la interfaz formal en scripts/lib/tracing-processor-interface.js. Cargar cuando se implementa un exportador de trazas nuevo, se integra observabilidad externa, o se usa MultiProcessor.
version: "1.0.0"
herramientasPermitidas: [Read, Write]
exclusiones:
  - "No cargar para leer trazas ya generadas en `.planning/traces/` — eso es lectura directa de JSONL con el Read tool o con `drift-detection`; este skill documenta cómo implementar nuevos exportadores, no cómo consumir trazas."
  - "No cargar para agregar observabilidad a hooks que no son parte del pipeline de trazabilidad — los hooks que registran eventos en `nudges.jsonl`, `alertas-persistentes.json` o archivos de estado no son TracingProcessors; este skill es exclusivo para exportadores de spans del ciclo de vida de agentes."
  - "No cargar para depurar por qué un agente falla — las trazas son observabilidad, no diagnóstico de bugs; para bugs usar `depurador-swl` o revisar los logs del hook directamente."
  - "No cargar si el exportador objetivo ya está implementado en `hooks/lib/otlp-exporter.js` y solo necesita configuración vía variable de entorno — los exportadores existentes se activan con variables opt-in, no requieren código nuevo."
evolvable: true
evolvable_scope:
  - content
  - examples
  - anti-patterns
---
# Habilidad: TracingProcessor — Exportadores de Trazas

## Propósito del patrón

El sistema SWL ya registra spans en `.planning/traces/YYYY-MM-DD.jsonl` vía
`hooks/lib/otlp-exporter.js`. Sin la interfaz formal, agregar un segundo
destino de exportación (Langfuse, Jaeger, consola) requeriría modificar ese
archivo — violando el principio Open/Closed.

**TracingProcessor** resuelve esto: define un contrato que cualquier exportador
debe cumplir. El núcleo (`hooks/telemetria-agentes.js`) solo conoce la interfaz;
los destinos concretos son plugins intercambiables.

Beneficios:
- Agregar destinos sin tocar código existente.
- Combinar múltiples destinos con `MultiProcessor` (patrón Composite).
- Testear exportadores de forma aislada.

---

## Contrato de la interfaz

Archivo: `scripts/lib/tracing-processor-interface.js`

| Método | Cuándo se invoca | Responsabilidad |
|--------|-----------------|----------------|
| `onTraceStart(traza)` | Al iniciar una traza de alto nivel | Registrar inicio, crear buffer si aplica |
| `onTraceEnd(traza)` | Al finalizar una traza completa | Exportar la traza completa, liberar buffer |
| `onSpanStart(span)` | Al iniciar un span individual | Registrar inicio del span |
| `onSpanEnd(span)` | Al finalizar un span (datos completos disponibles) | Punto principal de exportación |
| `shutdown()` | Al detener la aplicación | Vaciar buffers, cerrar archivos/conexiones |
| `forceFlush()` | Antes de shutdown o cuando se necesita garantía | Forzar volcado de colas internas |

Reglas del contrato:
- Los métodos deben retornar rápidamente. Usar colas internas para I/O lento.
- Los errores internos no deben propagarse al caller.
- Todos los métodos son síncronos por contrato; las subclases usan colas async internamente.

---

## Cómo crear un exportador alterno

### Paso 1 — Extender la clase abstracta

```js
'use strict';
const { TracingProcessor } = require('../../scripts/lib/tracing-processor-interface');

class MiExportador extends TracingProcessor {
  constructor(opciones = {}) {
    super();
    this._buffer = [];
    this._destino = opciones.destino || 'https://mi-backend.example.com/traces';
  }

  onTraceStart(traza) { /* opcional: registrar inicio */ }
  onTraceEnd(traza)   { this._enviarBatch(); }
  onSpanStart(span)   { /* opcional */ }

  onSpanEnd(span) {
    this._buffer.push(span);
    if (this._buffer.length >= 50) this._enviarBatch();
  }

  shutdown()    { this._enviarBatch(); }
  forceFlush()  { this._enviarBatch(); }

  _enviarBatch() {
    const lote = this._buffer.splice(0);
    if (!lote.length) return;
    // Envío HTTP fire-and-forget
    try {
      const https = require('https');
      const body  = JSON.stringify(lote);
      const req   = https.request(this._destino, { method: 'POST',
        headers: { 'Content-Type': 'application/json' } });
      req.on('error', () => {});
      req.write(body);
      req.end();
    } catch (_) {}
  }
}

module.exports = { MiExportador };
```

### Paso 2 — Verificar la implementación

```js
const { TracingProcessor } = require('../../scripts/lib/tracing-processor-interface');
const { MiExportador } = require('./mi-exportador');

console.log(TracingProcessor.esProcesadorValido(new MiExportador())); // → true
```

### Paso 3 — Registrar en el pipeline (opcional)

```js
const { MultiProcessor } = require('../../scripts/lib/tracing-processor-interface');
const { OtlpLocalProcessor } = require('../../hooks/lib/otlp-exporter');
const { MiExportador } = require('./mi-exportador');

const pipeline = new MultiProcessor([
  new OtlpLocalProcessor(process.cwd()),   // siempre activo
  new MiExportador({ destino: process.env.TRACES_ENDPOINT }),
]);
```

---

## Patrón Composite con MultiProcessor

`MultiProcessor` implementa `TracingProcessor` y propaga cada evento a N hijos.

**Cuándo usarlo:**
- Necesitas enviar spans a más de un destino simultáneamente.
- Quieres activar un exportador de consola durante debugging sin modificar el pipeline.
- Construyes un pipeline configurable donde los destinos se leen de variables de entorno.

**Comportamiento ante errores:** si un hijo lanza en `onSpanEnd`, `MultiProcessor`
captura la excepción y continúa con los demás hijos. Ningún fallo individual
interrumpe el pipeline completo.

```js
const multi = new MultiProcessor([exportadorA, exportadorB]);
multi.addProcessor(exportadorC);   // agregar en tiempo de ejecución
multi.removeProcessor(exportadorB); // eliminar por referencia
```

---

## Casos de uso reales

### Exportador a Langfuse (mediano plazo)

Langfuse acepta trazas vía HTTP. Crear `scripts/lib/langfuse-processor.js`
extendiendo `TracingProcessor`. En `onSpanEnd`, formatear el span al schema
de Langfuse y encolarlo. En `shutdown`, vaciar la cola con `forceFlush`.

Variables de entorno necesarias: `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`,
`LANGFUSE_HOST`. El procesador solo se activa si `LANGFUSE_PUBLIC_KEY` está
definida (patrón opt-in de `CLAUDE.md`).

### Exportador a Jaeger

Jaeger acepta spans en formato OpenTelemetry JSON. Los spans de swl-ses ya
tienen la estructura correcta (`traceId`, `spanId`, `nombre`, `duracionMs`).
El exportador debe mapear `estado: 'ERROR'` al código de status OTLP `2`.

Endpoint: `JAEGER_OTLP_ENDPOINT` (por defecto `http://localhost:4318/v1/traces`).

### Exportador para métricas agregadas

En lugar de persistir spans crudos, un exportador puede agregar en memoria:
contar spans por tipo, sumar duraciones, detectar anomalías. En `onTraceEnd`
emite las métricas agregadas a `.planning/evolution/metricas.json` usando
`atomicWriteJSON` de `hooks/lib/atomic-write.js`.

---

## Anti-patrones

| Anti-patrón | Problema | Solución |
|-------------|----------|---------|
| Exportador que bloquea `onSpanEnd` con I/O síncrono lento | Ralentiza todos los agentes | Usar cola interna y enviar en batch desde `onTraceEnd` o un setInterval |
| Exportador que relanza excepciones de red | Interrumpe el pipeline completo | Capturar con try/catch, registrar el error internamente |
| Exportador sin `shutdown` / `forceFlush` implementados | Spans en buffer se pierden al cerrar el proceso | Implementar ambos métodos; `shutdown` debe vaciar el buffer |
| Exportador que modifica el span recibido | Efectos secundarios para otros procesadores en el pipeline | Clonar el span antes de mutarlo: `const s = { ...span }` |
| Exportador activado incondicionalmente en producción | Agrega latencia sin config del usuario | Patrón opt-in: `if (!process.env.MI_ENDPOINT) return` al inicio |

---

## Relación con el resto del sistema

| Componente | Relación |
|-----------|----------|
| `scripts/lib/tracing-processor-interface.js` | Define las clases abstractas `TracingProcessor`, `TracingExporter` y `MultiProcessor` |
| `hooks/lib/otlp-exporter.js` | Implementación concreta `OtlpLocalProcessor` + funciones sueltas (compatibilidad) |
| `scripts/lib/span-schema.js` | Taxonomía de 13 tipos de span; los spans que llegan a `onSpanEnd` siguen este schema |
| `hooks/telemetria-agentes.js` | Produce los spans del ciclo de vida de agentes; es el emisor principal |
| `.planning/traces/YYYY-MM-DD.jsonl` | Destino por defecto del `OtlpLocalProcessor` |
| `hooks/lib/atomic-write.js` | Para exportadores que persisten estado (métricas, índices) — no usar para JSONL |

## Cuándo NO cargar

- Se leen o analizan trazas ya generadas en `.planning/traces/` — usar Read tool o `drift-detection`; este skill describe cómo construir exportadores nuevos, no cómo consumir el JSONL existente.
- Se agrega observabilidad a hooks que no emiten spans del ciclo de vida de agentes (hooks de perfilado, guardrails, auditoría) — esos hooks registran en sus propios archivos JSONL; `TracingProcessor` es para el pipeline de spans de `telemetria-agentes.js`.
- El exportador objetivo ya existe en `hooks/lib/otlp-exporter.js` y solo necesita activarse — los exportadores implementados se activan con la variable de entorno correspondiente (patrón opt-in); no requieren código nuevo.
- Se diagnostica por qué un agente produjo un resultado incorrecto — las trazas miden ciclo de vida y duración, no lógica; para bugs usar `depurador-swl` o los logs del hook directamente.

## Gotchas / Errores comunes no obvios

- **`onSpanEnd` con I/O síncrono bloquea el pipeline completo**: el exportador llama a `fetch()` o `fs.writeFileSync()` directamente en `onSpanEnd` y ralentiza todos los agentes porque cada span espera al I/O antes de continuar. Causa: `onSpanEnd` se ejecuta síncronamente en el mismo event loop. Solución: encolar los spans en un buffer interno y vaciar el buffer en `onTraceEnd` o con un `setInterval` — el anti-patrón está documentado explícitamente en la tabla del skill.
- **Exportador que mutua el span recibido afecta a otros procesadores en el pipeline**: un exportador agrega un campo `exportado: true` al span y el siguiente procesador en el `MultiProcessor` recibe el span modificado. Causa: los procesadores comparten el mismo objeto span por referencia. Solución: clonar el span antes de mutarlo (`const s = { ...span }`) — regla documentada en los anti-patrones del skill.
- **`shutdown()` no implementado provoca pérdida de spans en buffer al cerrar el proceso**: el proceso del hook termina y los spans acumulados en la cola interna se descartan. Causa: implementar solo `onSpanEnd` y `onTraceEnd` sin `shutdown`. Solución: `shutdown` debe vaciar el buffer síncronamente (o retornar una Promise que resuelve al vaciar); Claude Code llama a `shutdown` en el proceso de cierre del hook.
- **Exportador activado sin variable de entorno opt-in en todos los entornos**: el exportador a Langfuse se activa incluso en proyectos que no configuraron `LANGFUSE_PUBLIC_KEY`, añadiendo latencia de red sin beneficio. Causa: no implementar el guard de opt-in al inicio de `onSpanEnd`. Solución: todo exportador externo debe comenzar con `if (!process.env.MI_ENDPOINT) return` — el patrón opt-in de CLAUDE.md es obligatorio para integraciones enterprise.
