npm package: @hemia/app-context
Description: Manejo centralizado de contexto y sesión para apps Hemia.
Author: Hemia Technologies
Latest version: 0.0.6

# @hemia/app-context

Middleware y utilidades para gestionar contexto distribuido compatible con **OpenTelemetry** en aplicaciones backend Node.js/Express. Proporciona trazabilidad automática, observabilidad profunda y correlación de logs con esquema optimizado para **ClickHouse**.

## 🚀 Características principales

✅ **Compatible con OpenTelemetry** - Spans y Logs según estándar OTEL  
✅ **Esquema ClickHouse nativo** - Diseñado para `otel_traces` y `otel_logs`  
✅ **Correlación automática** - TraceId propagado via AsyncLocalStorage  
✅ **Decorador `@Trace`** - Captura automática de métodos como spans  
✅ **Logger contextual** - Logs correlacionados automáticamente  
✅ **Observabilidad profunda** - Memory, CPU, Host, User tracking  
✅ **Zero-config para servicios** - Contexto propagado automáticamente  

---

## 📦 Instalación

```bash
npm install @hemia/app-context uuid
npm install --save-dev @types/uuid
```

---

## 🎯 Uso básico

### 1. Setup del Middleware (Express)

```typescript
import express from 'express';
import { appContextMiddleware } from '@hemia/app-context';

const app = express();
app.use(express.json());

// Middleware de contexto OpenTelemetry
app.use('/api', 
  appContextMiddleware(
    'API',              // checkpoint
    'Gateway',          // functionCode
    (context, res) => {
      // Callback al finalizar: enviar trazas a ClickHouse/Collector
      console.log(`✅ Trace ${context.traceId} completado`);
      console.log(`   Spans: ${context.traceContext.spans.length}`);
      console.log(`   Logs: ${context.traceContext.logs.length}`);
      
      // Aquí envías a tu backend de observabilidad
      // sendToClickHouse(context.traceContext);
    },
    undefined,          // traceId (opcional, para continuar trace)
    ['api', 'v1']       // tags adicionales
  )
);

app.listen(3000);
```

### 2. Acceder al contexto en cualquier parte

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

export class UserService {
  async getUser(id: string) {
    const context = asyncContext.getStore();
    
    console.log(`TraceId: ${context?.traceId}`);
    console.log(`User: ${context?.user?.email}`);
    
    // Tu lógica aquí
  }
}
```

### 3. Usar decorador `@Trace` (Recomendado)

```typescript
import { Trace } from './decorators/Trace';
import { logger } from './logger/ContextualLogger';

export class UserService {
  
  @Trace({ 
    name: 'UserService.findById',
    kind: 'SPAN_KIND_CLIENT',
    attributes: { 'db.system': 'postgresql' }
  })
  async findById(id: string): Promise<User> {
    logger.info('Buscando usuario', { userId: id });
    
    const user = await db.query('SELECT * FROM users WHERE id = $1', [id]);
    
    if (!user) {
      logger.warn('Usuario no encontrado', { userId: id });
      throw new Error('User not found');
    }
    
    return user;
  }
}
```

### 4. Logger contextual (auto-correlacionado)

```typescript
import { logger } from './logger/ContextualLogger';

export class PaymentService {
  async processPayment(amount: number) {
    logger.info('Procesando pago', { amount });
    
    try {
      const result = await stripe.charge(amount);
      logger.info('Pago exitoso', { transactionId: result.id });
      return result;
    } catch (error: any) {
      logger.error('Error en pago', { error: error.message });
      throw error;
    }
  }
}
```

---

## 🏗️ Arquitectura OpenTelemetry

### Estructura de datos

```typescript
IAppContext {
  // Identificadores
  traceId: string              // UUID compartido por toda la transacción
  spanId: string               // UUID del span actual
  parentSpanId: string         // UUID del span padre
  serviceName: string          // Nombre del servicio
  
  // Contexto OpenTelemetry completo
  traceContext: {
    spans: TraceSpan[]         // Array de spans capturados
    logs: TraceLog[]           // Array de logs correlacionados
    resourceAttributes: {...}  // Metadatos del servicio/host
  }
  
  // Datos del request
  user?: IUserData             // Usuario autenticado
  userId?: string
  language: string
  
  // Telemetría del host
  host: IHostInfo              // hostname, ip, platform, cpuUsage
  memory: IMemoryInfo          // rss, heapTotal, heapUsed
  application: IApplicationInfo
  environment: IEnvironmentInfo
}
```

### TraceSpan (compatible con ClickHouse)

```typescript
interface TraceSpan {
  Timestamp: string            // ISO 8601
  TraceId: string
  SpanId: string
  ParentSpanId: string
  ServiceName: string          // ej: "user-service"
  SpanName: string             // ej: "UserService.findById"
  SpanKind: SpanKindType       // SERVER | CLIENT | INTERNAL
  DurationUInt64: bigint       // Nanosegundos
  StatusCode: StatusCodeType   // OK | ERROR | UNSET
  StatusMessage: string
  SpanAttributes: {            // Datos específicos del span
    'http.method': 'GET',
    'http.status_code': '200',
    'user.id': '123',
    'db.system': 'postgresql',
    ...
  }
  ResourceAttributes: {        // Metadatos del servicio
    'service.name': 'api',
    'host.name': 'server-01',
    'process.runtime.memory.rss': '52428800',
    ...
  }
  EventsNested: SpanEvent[]    // Excepciones, logs anidados
  LinksNested: SpanLink[]      // Links a otros traces
}
```

---

## 🎨 Decorador @Trace (Implementación sugerida)

> **Nota:** Este paquete proporciona el contexto (`asyncContext`) y las interfaces (`TraceSpan`, `SpanEvent`), pero el decorador `@Trace` debe implementarse en tu proyecto.

### Implementación recomendada del decorador

Crea en tu proyecto `src/decorators/Trace.ts`:

```typescript
import { asyncContext, TraceSpan, SpanEvent, SpanKindType } from '@hemia/app-context';
import { v4 as getUUID } from 'uuid';

export interface TraceOptions {
  name?: string;
  kind?: SpanKindType;
  attributes?: Record<string, string>;
}

export function Trace(options?: TraceOptions) {
  return function (target: any, key: string, descriptor: PropertyDescriptor): void {
    const originalMethod = descriptor.value;

    if (typeof originalMethod !== 'function') {
      throw new Error(`@Trace can only be applied to methods`);
    }

    descriptor.value = async function (...args: any[]) {
      const context = asyncContext.getStore();
      
      if (!context) {
        console.warn('[Trace] No active context. Skipping trace.');
        return await originalMethod.apply(this, args);
      }

      const spanId = getUUID();
      const className = this?.constructor?.name || target.name || 'UnknownClass';
      const spanName = options?.name || `${className}.${key}`;
      const startTime = process.hrtime.bigint();

      const span: TraceSpan = {
        Timestamp: new Date().toISOString(),
        TraceId: context.traceId,
        SpanId: spanId,
        ParentSpanId: context.spanId,
        TraceState: '',
        ServiceName: context.serviceName,
        SpanName: spanName,
        SpanKind: options?.kind || 'SPAN_KIND_INTERNAL',
        DurationUInt64: BigInt(0),
        StatusCode: 'STATUS_CODE_UNSET',
        StatusMessage: '',
        SpanAttributes: {
          'code.function': key,
          'code.namespace': className,
          ...options?.attributes,
        },
        ResourceAttributes: context.traceContext.resourceAttributes,
        EventsNested: [],
        LinksNested: [],
      };

      const previousSpanId = context.spanId;
      context.spanId = spanId;

      try {
        const result = await originalMethod.apply(this, args);
        span.StatusCode = 'STATUS_CODE_OK';
        return result;

      } catch (error: any) {
        const exceptionEvent: SpanEvent = {
          Timestamp: new Date().toISOString(),
          Name: 'exception',
          Attributes: {
            'exception.type': error.constructor?.name || 'Error',
            'exception.message': error.message || 'Unknown error',
            'exception.stacktrace': (error.stack || '').substring(0, 2000),
          },
        };
        span.EventsNested.push(exceptionEvent);
        span.StatusCode = 'STATUS_CODE_ERROR';
        span.StatusMessage = error.message || 'Unhandled exception';
        throw error;

      } finally {
        const endTime = process.hrtime.bigint();
        span.DurationUInt64 = endTime - startTime;
        context.traceContext.spans.push(span);
        context.spanId = previousSpanId;
      }
    };
  };
}
```

### Ejemplos de uso del decorador

```typescript
export class UserService {
  
  // Sin opciones (usa defaults)
  @Trace()
  async findAll(): Promise<User[]> {
    return await db.query('SELECT * FROM users');
  }

  // Con nombre personalizado
  @Trace({ name: 'DB:GetUserById' })
  async findById(id: string): Promise<User> {
    return await db.findOne(id);
  }

  // Span de cliente (llamada externa)
  @Trace({ 
    kind: 'SPAN_KIND_CLIENT',
    attributes: { 'db.system': 'postgresql' }
  })
  async saveUser(user: User): Promise<void> {
    await db.save(user);
  }
}
```

---

## 📝 Logger Contextual (Implementación sugerida)

> **Nota:** Este paquete proporciona `addLogToContext()` como función helper. El logger de clase es una implementación sugerida para tu proyecto.

### Opción 1: Usar `addLogToContext()` directamente (incluido en el paquete)

```typescript
import { asyncContext, addLogToContext } from '@hemia/app-context';

const context = asyncContext.getStore();
if (context) {
  addLogToContext(context, 'INFO', 'Usuario encontrado', { userId: '123' });
  addLogToContext(context, 'ERROR', 'Falló la conexión', { error: 'timeout' });
}
```

### Opción 2: Implementar logger de clase (recomendado)

Crea en tu proyecto `src/logger/ContextualLogger.ts`:

```typescript
import { asyncContext, TraceLog, SeverityTextType } from '@hemia/app-context';

const SEVERITY_MAP: Record<SeverityTextType, number> = {
  DEBUG: 5,
  INFO: 9,
  WARN: 13,
  ERROR: 17,
  FATAL: 21,
};

export class ContextualLogger {
  private log(severity: SeverityTextType, message: string, attributes?: Record<string, any>): void {
    const context = asyncContext.getStore();
    
    if (!context) {
      console.warn('[Logger] No active context. Log not traced.');
      console.log(`[${severity}] ${message}`, attributes);
      return;
    }

    const logEntry: TraceLog = {
      Timestamp: new Date().toISOString(),
      TraceId: context.traceId,
      SpanId: context.spanId,
      TraceFlags: 1,
      SeverityText: severity,
      SeverityNumber: SEVERITY_MAP[severity],
      ServiceName: context.serviceName,
      Body: message,
      LogAttributes: this.stringifyAttributes(attributes || {}),
      ResourceAttributes: context.traceContext.resourceAttributes,
    };

    context.traceContext.logs.push(logEntry);

    if (process.env.NODE_ENV !== 'production') {
      console.log(`[${severity}] [${context.traceId.substring(0, 8)}...] ${message}`, attributes || '');
    }
  }

  private stringifyAttributes(attrs: Record<string, any>): Record<string, string> {
    const result: Record<string, string> = {};
    for (const [key, value] of Object.entries(attrs)) {
      if (value === undefined || value === null) {
        result[key] = '';
      } else if (typeof value === 'string') {
        result[key] = value;
      } else {
        try {
          result[key] = JSON.stringify(value);
        } catch {
          result[key] = String(value);
        }
      }
    }
    return result;
  }

  debug(message: string, attributes?: Record<string, any>): void {
    this.log('DEBUG', message, attributes);
  }

  info(message: string, attributes?: Record<string, any>): void {
    this.log('INFO', message, attributes);
  }

  warn(message: string, attributes?: Record<string, any>): void {
    this.log('WARN', message, attributes);
  }

  error(message: string, attributes?: Record<string, any>): void {
    this.log('ERROR', message, attributes);
  }

  fatal(message: string, attributes?: Record<string, any>): void {
    this.log('FATAL', message, attributes);
  }
}

export const logger = new ContextualLogger();
```

---

## 🔧 API Reference

### Funciones exportadas

```typescript
// Middleware principal
appContextMiddleware(
  checkPoint: string,
  functionCode: string,
  onFinishCallback?: (context: IAppContext, res: Response) => void,
  traceId?: string,
  tags?: string[]
): RequestHandler

// Manejo de errores
updateContextStackTrace(
  context: IAppContext,
  stage: string,
  error: unknown,
  errorCode: string
): void

// Agregar spans/logs manualmente
addSpanToContext(
  context: IAppContext,
  spanName: string,
  durationMs: number,
  options?: TraceOptions
): void

addLogToContext(
  context: IAppContext,
  severityText: SeverityTextType,
  body: string,
  logAttributes?: Record<string, string>
): void

// Helpers OpenTelemetry
userToSpanAttributes(user?: IUserData): Record<string, string>
generateResourceAttributes(host, app, env, memory?): Record<string, string>
createTraceSpan(options): TraceSpan
createTraceLog(options): TraceLog
```

### Headers utilizados

| Header | Descripción |
|--------|-------------|
| `x-trace-id` | ID único del trace (propagado entre servicios) |
| `x-span-id` | ID del span actual (generado por cada servicio) |

---

## 📊 Ejemplo de jerarquía de Spans

```
TraceId: abc-123-def-456

├── API.Gateway (SPAN_KIND_SERVER) ────────────── 250ms
│   ├── UserController.getUser ────────────────── 240ms
│   │   ├── UserService.findById ──────────────── 80ms
│   │   │   └── UserRepository.query (CLIENT) ── 75ms
│   │   │
│   │   └── EmailService.sendWelcome (CLIENT) ── 150ms
│   │
│   └── API.Gateway.response ──────────────────── 250ms
│
└── [Logs correlacionados por TraceId]
    • INFO: "Request received"
    • DEBUG: "Querying database"
    • INFO: "User found"
    • INFO: "Sending welcome email"
    • INFO: "Request completed"
```

---

## 🎯 SpanKind Reference

| SpanKind | Cuándo usar | Ejemplo |
|----------|-------------|---------|
| `SPAN_KIND_SERVER` | Endpoint que recibe requests | Controllers HTTP |
| `SPAN_KIND_CLIENT` | Llamadas salientes (DB, HTTP, etc) | Repositories, HTTP clients |
| `SPAN_KIND_INTERNAL` | Lógica interna del servicio | Services, Validators |
| `SPAN_KIND_PRODUCER` | Publicar mensajes | Kafka Producer |
| `SPAN_KIND_CONSUMER` | Consumir mensajes | Kafka Consumer |

---

## 📦 Interfaces principales

```typescript
interface IAppContext {
  traceContext: TraceContext;
  traceId: string;
  spanId: string;
  parentSpanId: string;
  serviceName: string;
  user?: IUserData;
  userId?: string;
  // ... más campos
}

interface TraceContext {
  spans: TraceSpan[];
  logs: TraceLog[];
  resourceAttributes: Record<string, string>;
}

interface IUserData {
  idUser: string;
  email: string;
  idRole: string;
  nameRole: string;
  roles: string[];
  permisos: string[];
  profile?: IUserProfile;
}
```

---

## 🗄️ Schema ClickHouse

Este paquete genera spans y logs compatibles con:

```sql
CREATE TABLE otel_traces (
    Timestamp DateTime64(9),
    TraceId String,
    SpanId String,
    ParentSpanId String,
    ServiceName LowCardinality(String),
    SpanName LowCardinality(String),
    SpanKind LowCardinality(String),
    DurationUInt64 UInt64,
    StatusCode LowCardinality(String),
    SpanAttributes Map(LowCardinality(String), String),
    ResourceAttributes Map(LowCardinality(String), String),
    EventsNested Nested(...),
    LinksNested Nested(...)
) ENGINE = MergeTree()
ORDER BY (ServiceName, SpanName, toUnixTimestamp(Timestamp), TraceId);

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

---

## 🚀 Roadmap

- [ ] Exporters para ClickHouse directo
- [ ] Integración con OpenTelemetry Collector
- [ ] Sampler configurable
- [ ] Métricas automáticas (latency, error rate)
- [ ] Dashboard de visualización

---

## 📋 Requisitos

- Node.js >= 16
- Express >= 4.x
- TypeScript >= 4.x (recomendado)

---

## 📄 Licencia

MIT

---

## 👥 Autor

**Hemia Technologies**

🔗 Más información: [hemia.com](https://hemia.com)