npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@hemia/app-context

v0.0.6

Published

Manejo centralizado de contexto y sesión para apps Hemia.

Readme

@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

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

🎯 Uso básico

1. Setup del Middleware (Express)

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

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)

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)

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

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)

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:

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

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)

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:

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

// 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

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:

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