npm package: @hemia/trace-manager
Description: Gestor de trazas para registrar logs, errores y evento
Author: Hemia Technologies
Latest version: 0.0.9

# @hemia/trace-manager

Sistema de trazabilidad distribuida basado en **OpenTelemetry** para aplicaciones Node.js. Proporciona decoradores automáticos, logging contextual correlacionado con trazas y compatibilidad con ClickHouse para análisis de observabilidad.

---

## 🚀 Características

- ✅ **Decorador `@Trace`** - Instrumentación automática de métodos con captura de inputs/outputs
- ✅ **AsyncLocalStorage** - Propagación automática de contexto sin pasar parámetros manualmente
- ✅ **Logger Contextual** - Logs automáticamente correlacionados con `traceId` y `spanId`
- ✅ **Formato OpenTelemetry** - Compatible con estándares de observabilidad
- ✅ **ClickHouse Ready** - Esquema optimizado para análisis en ClickHouse
- ✅ **Captura de Errores** - Excepciones registradas como eventos en spans
- ✅ **Jerarquía de Spans** - Parent-Child spans automáticos por nivel de invocación

---

## 📦 Instalación

```bash
npm install @hemia/trace-manager @hemia/app-context
```

---

## 🎯 Componentes principales

### ✅ Decorador `@Trace()`

Instrumenta automáticamente métodos creando spans de OpenTelemetry con captura de inputs, outputs y errores.

#### **Características:**

- 🔹 Crea span automáticamente con `SpanId` único
- 🔹 Captura **metadata** de argumentos (tipos, cantidad) - no valores completos
- 🔹 Captura **metadata** de resultados (tipo, isArray, length) - no valores completos
- 🔹 Registra excepciones como eventos (`EventsNested`)
- 🔹 Calcula duración en nanosegundos (`DurationUInt64`)
- 🔹 Propaga contexto automáticamente a métodos hijos
- 🔹 **Privacy-first**: No serializa valores sensibles, solo metadata

#### **Uso básico:**

```ts
import { Trace } from '@hemia/trace-manager';

class UserService {
  @Trace()
  async createUser(userData: any) {
    // El decorador captura automáticamente:
    // - Metadata de input: tipo, cantidad de args (NO valores completos)
    // - Metadata de output: tipo, isArray, length (NO valores completos)
    // - Duración de ejecución en nanosegundos
    // - Excepciones si ocurren (con stacktrace)
    
    const user = await this.repository.save(userData);
    return user;
  }

  @Trace({ name: 'validate-user-email' })
  private async validateEmail(email: string) {
    // Span hijo automático (hereda ParentSpanId)
    return await this.emailValidator.check(email);
  }
}
```

#### **Opciones de configuración:**

```ts
interface TraceOptions {
  name?: string;                    // Nombre personalizado del span (default: ClassName.methodName)
  kind?: 'SPAN_KIND_INTERNAL'       // Tipo de span: INTERNAL, SERVER, CLIENT, PRODUCER, CONSUMER
       | 'SPAN_KIND_SERVER' 
       | 'SPAN_KIND_CLIENT'
       | 'SPAN_KIND_PRODUCER'
       | 'SPAN_KIND_CONSUMER';
  attributes?: Record<string, string>; // Atributos personalizados
}
```

#### **Ejemplo avanzado:**

```ts
class PaymentService {
  @Trace({ 
    name: 'process-payment-stripe',
    kind: 'SPAN_KIND_CLIENT',
    attributes: { 'payment.provider': 'stripe' }
  })
  async processPayment(amount: number, currency: string) {
    // Span con atributos personalizados
    return await this.stripeClient.charge(amount, currency);
  }
}
```

---

### ✅ Logger Contextual

Logger que automáticamente correlaciona logs con el `traceId` y `spanId` activo, permitiendo rastrear logs específicos dentro de una traza distribuida.

#### **Características:**

- 🔹 Correlación automática con `TraceId` y `SpanId`
- 🔹 Niveles de severidad: `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`
- 🔹 Atributos personalizados en cada log
- 🔹 Compatible con formato OpenTelemetry para ClickHouse

#### **Uso:**

```ts
import { logger } from '@hemia/trace-manager';

class OrderService {
  @Trace()
  async placeOrder(order: Order) {
    logger.info('Processing order', { orderId: order.id, amount: order.total });
    
    try {
      const result = await this.payment.charge(order.total);
      logger.info('Payment successful', { transactionId: result.id });
      return result;
    } catch (error) {
      logger.error('Payment failed', { error: error.message });
      throw error;
    }
  }
}
```

#### **Métodos disponibles:**

```ts
logger.debug(message: string, attributes?: Record<string, any>): void
logger.info(message: string, attributes?: Record<string, any>): void
logger.warn(message: string, attributes?: Record<string, any>): void
logger.error(message: string, attributes?: Record<string, any>): void
logger.fatal(message: string, attributes?: Record<string, any>): void
```

---

## 🏗️ Estructura de Datos

### **TraceSpan** (Span de OpenTelemetry)

Mapea directamente con la tabla `otel_traces` de ClickHouse:

```ts
interface TraceSpan {
  Timestamp: string;               // ISO 8601 - DateTime64(9)
  TraceId: string;                 // ID único de la traza
  SpanId: string;                  // ID único del span
  ParentSpanId: string;            // ID del span padre (jerarquía)
  TraceState: string;              // Estado de propagación W3C
  ServiceName: string;             // Nombre del servicio
  SpanName: string;                // Ej: "UserService.createUser"
  SpanKind: 'SPAN_KIND_INTERNAL' | 'SPAN_KIND_SERVER' | 'SPAN_KIND_CLIENT' | ...;
  DurationUInt64: bigint;          // Duración en nanosegundos
  StatusCode: 'STATUS_CODE_OK' | 'STATUS_CODE_ERROR' | 'STATUS_CODE_UNSET';
  StatusMessage: string;
  SpanAttributes: Record<string, string>;        // app.method.args, app.method.result, etc.
  ResourceAttributes: Record<string, string>;    // host.name, service.name, etc.
  EventsNested: SpanEvent[];       // Excepciones, logs puntuales
  LinksNested: SpanLink[];         // Enlaces a otras trazas
}
```

### **TraceLog** (Log de OpenTelemetry)

Mapea directamente con la tabla `otel_logs` de ClickHouse:

```ts
interface TraceLog {
  Timestamp: string;               // ISO 8601
  TraceId: string;                 // Correlación automática con span activo
  SpanId: string;                  // Span donde ocurrió el log
  TraceFlags: number;              // 1 = sampled
  SeverityText: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'FATAL';
  SeverityNumber: number;          // 5, 9, 13, 17, 21
  ServiceName: string;
  Body: string;                    // Mensaje del log
  LogAttributes: Record<string, string>;         // Atributos custom
  ResourceAttributes: Record<string, string>;    // Metadata del servicio
}
```

---

## 🔗 Integración con `@hemia/app-context`

Este paquete utiliza `AsyncLocalStorage` a través de `@hemia/app-context` para mantener el contexto de trazabilidad sin necesidad de pasarlo explícitamente como parámetro.

### **Requisitos:**

El contexto debe ser inicializado previamente usando `@hemia/app-context` (generalmente en un middleware HTTP):

```ts
import { asyncContext } from '@hemia/app-context';

// En tu middleware o inicialización
const context = {
  traceId: generateTraceId(),
  spanId: generateSpanId(),
  serviceName: 'user-service',
  traceContext: {
    resourceAttributes: {
      'service.name': 'user-service',
      'host.name': os.hostname(),
    },
    spans: [],
    logs: []
  }
};

await asyncContext.run(context, async () => {
  // Todo el código dentro hereda este contexto automáticamente
  await handleRequest(req, res);
});
```

---

## 📊 Esquema ClickHouse

### Tabla: `otel_traces`

```sql
CREATE TABLE otel_traces (
    Timestamp DateTime64(9) CODEC(Delta, ZSTD(1)),
    TraceId String CODEC(ZSTD(1)),
    SpanId String CODEC(ZSTD(1)),
    ParentSpanId String CODEC(ZSTD(1)),
    ServiceName LowCardinality(String) CODEC(ZSTD(1)),
    SpanName LowCardinality(String) CODEC(ZSTD(1)),
    SpanKind LowCardinality(String) CODEC(ZSTD(1)),
    DurationUInt64 UInt64 CODEC(ZSTD(1)),
    StatusCode LowCardinality(String) CODEC(ZSTD(1)),
    SpanAttributes Map(LowCardinality(String), String) CODEC(ZSTD(1)),
    ResourceAttributes Map(LowCardinality(String), String) CODEC(ZSTD(1)),
    EventsNested Nested (
        Timestamp DateTime64(9),
        Name LowCardinality(String),
        Attributes Map(LowCardinality(String), String)
    ) CODEC(ZSTD(1))
) ENGINE = MergeTree()
PARTITION BY toDate(Timestamp)
ORDER BY (ServiceName, SpanName, toUnixTimestamp(Timestamp), TraceId);
```

### Tabla: `otel_logs`

```sql
CREATE TABLE otel_logs (
    Timestamp DateTime64(9) CODEC(Delta, ZSTD(1)),
    TraceId String CODEC(ZSTD(1)),
    SpanId String CODEC(ZSTD(1)),
    SeverityText LowCardinality(String) CODEC(ZSTD(1)),
    SeverityNumber Int32 CODEC(ZSTD(1)),
    ServiceName LowCardinality(String) CODEC(ZSTD(1)),
    Body String CODEC(ZSTD(1)),
    LogAttributes Map(LowCardinality(String), String) CODEC(ZSTD(1)),
    ResourceAttributes Map(LowCardinality(String), String) CODEC(ZSTD(1))
) ENGINE = MergeTree()
PARTITION BY toDate(Timestamp)
ORDER BY (ServiceName, SeverityText, toUnixTimestamp(Timestamp), TraceId);
```

---

## 🔍 Ejemplo completo

```ts
import { Trace, logger } from '@hemia/trace-manager';
import { asyncContext } from '@hemia/app-context';

class OrderController {
  @Trace({ name: 'http-create-order', kind: 'SPAN_KIND_SERVER' })
  async createOrder(req: Request) {
    logger.info('Order request received', { userId: req.userId });
    
    const order = await this.orderService.create(req.body);
    
    logger.info('Order created successfully', { orderId: order.id });
    return order;
  }
}

class OrderService {
  @Trace()
  async create(orderData: any) {
    // Span hijo automático (ParentSpanId = span del controller)
    logger.debug('Validating order data');
    
    await this.validate(orderData);
    const order = await this.repository.save(orderData);
    
    logger.info('Order persisted', { orderId: order.id });
    return order;
  }

  @Trace({ name: 'validate-order-rules' })
  private async validate(data: any) {
    // Span nieto (hijo del método create)
    if (!data.items?.length) {
      logger.error('Validation failed: no items');
      throw new Error('Order must have items');
    }
  }
}
```

**Resultado en ClickHouse:**

3 spans jerárquicos:
1. `http-create-order` (root, SPAN_KIND_SERVER)
2. `OrderService.create` (child, SPAN_KIND_INTERNAL)
3. `validate-order-rules` (grandchild, SPAN_KIND_INTERNAL)

Y múltiples logs correlacionados con el mismo `TraceId`.

---

## 📝 Buenas Prácticas

1. **Usar `@Trace()` en capas de negocio críticas**: Controllers, Services, Repositories
2. **Logger en puntos de decisión**: Validaciones, llamadas externas, errores
3. **Atributos significativos**: Agregar IDs de entidades, estados, providers
4. **Nombres descriptivos**: Usar `name` en `@Trace()` para operaciones complejas
5. **Metadata approach**: El decorador captura metadata (tipos, cantidad) en lugar de valores completos
6. **Privacy by default**: No se serializan valores sensibles como passwords o tokens

---

## 🛠️ Utilidades

### Helpers para SpanAttributes (Metadata Approach)

El paquete incluye utilidades para agregar metadata en lugar de serializar datos completos, siguiendo las mejores prácticas de observabilidad:

#### **`addArgsMetadata(attributes, args)`**
Agrega metadata de argumentos de función:
```ts
import { addArgsMetadata } from '@hemia/trace-manager';

const args = ['user@test.com', 123, { name: 'John' }];
addArgsMetadata(span.SpanAttributes, args);

// Resultado:
// {
//   "app.method.args.count": "3",
//   "app.method.args.types": "string,number,object"
// }
```

#### **`addResultMetadata(attributes, result)`**
Agrega metadata de resultado de función:
```ts
import { addResultMetadata } from '@hemia/trace-manager';

const result = [{ id: 1 }, { id: 2 }];
addResultMetadata(span.SpanAttributes, result);

// Resultado:
// {
//   "app.method.result.type": "object",
//   "app.method.result.isArray": "true",
//   "app.method.result.length": "2"
// }
```

#### **`addRequestBodyMetadata(attributes, body, prefix?)`**
Agrega metadata de Request body (para middlewares HTTP):
```ts
import { addRequestBodyMetadata } from '@hemia/trace-manager';

const reqBody = { email: 'user@test.com', password: '***' };
addRequestBodyMetadata(span.SpanAttributes, reqBody);

// Resultado:
// {
//   "http.request.body.exists": "true",
//   "http.request.body.type": "object",
//   "http.request.body.keys": "email,password",
//   "http.request.body.keyCount": "2"
// }
```

#### **`addResponseBodyMetadata(attributes, body, isError, prefix?)`**
Agrega metadata de Response body con captura inteligente de errores:
```ts
import { addResponseBodyMetadata } from '@hemia/trace-manager';

const errorBody = { 
  code: 'AUTH_ERROR', 
  message: 'Invalid credentials' 
};

addResponseBodyMetadata(
  span.SpanAttributes, 
  errorBody, 
  true // isError = true activa captura de mensaje
);

// Resultado:
// {
//   "http.response.body.exists": "true",
//   "http.response.body.type": "object",
//   "http.response.body.keys": "code,message",
//   "http.response.body.keyCount": "2",
//   "http.response.error.code": "AUTH_ERROR",
//   "http.response.error.message": "Invalid credentials"
// }
```

#### **`addObjectMetadata(attributes, data, prefix)`**
Agrega metadata genérica de cualquier objeto:
```ts
import { addObjectMetadata } from '@hemia/trace-manager';

const payload = { items: [1, 2, 3], total: 100 };
addObjectMetadata(span.SpanAttributes, payload, 'order.payload');

// Resultado:
// {
//   "order.payload.exists": "true",
//   "order.payload.type": "object",
//   "order.payload.keys": "items,total",
//   "order.payload.keyCount": "2"
// }
```

### Uso en Middleware Custom

```ts
import { 
  addRequestBodyMetadata, 
  addResponseBodyMetadata 
} from '@hemia/trace-manager';

export const customMiddleware = () => {
  return (req: Request, res: Response, next: NextFunction) => {
    // ... inicializar span ...

    res.on('finish', () => {
      const isError = res.statusCode >= 400;

      if (isError) {
        // Capturar metadata en lugar de cuerpos completos
        addRequestBodyMetadata(
          span.SpanAttributes,
          req.body
        );

        addResponseBodyMetadata(
          span.SpanAttributes,
          res.locals.responseBody,
          isError // Captura mensaje de error si existe
        );
      }
    });

    next();
  };
};
```

### Ventajas del Metadata Approach

✅ **Menor storage**: 10-20 bytes vs potencialmente KB de JSON  
✅ **Privacy by default**: No serializa valores sensibles (passwords, tokens)  
✅ **Mejor cardinality**: Más eficiente para queries en ClickHouse  
✅ **Suficiente para debugging**: Keys y tipos son suficientes para diagnosticar  
✅ **Consistencia**: Mismo approach en decoradores y middlewares  

### `sanitizeArgs()` y `toSafeJSON()`

Utilidades legacy que previenen:
- Serialización de objetos circulares
- Filtrado de Request/Response de Express
- Overflow de tamaño en ClickHouse

**⚠️ Nota**: El decorador `@Trace` ahora usa metadata en lugar de estas utilidades, pero siguen disponibles para compatibilidad.

---

## 📚 Dependencias

- `@hemia/app-context` (^0.0.6) - Manejo de contexto con AsyncLocalStorage
- `uuid` (^10.0.0) - Generación de IDs únicos para spans

## 🔄 Versión Actual

**v0.0.3** - Metadata approach implementado (Noviembre 2025)

---

## 📄 Licencia

MIT

---

## 👨‍💻 Autor

**Hemia Technologies**

---

## 🔗 Recursos

- [OpenTelemetry Specification](https://opentelemetry.io/docs/specs/otel/)
- [ClickHouse Documentation](https://clickhouse.com/docs)
- [AsyncLocalStorage Node.js](https://nodejs.org/api/async_context.html)