@hemia/trace-manager
v0.0.4
Published
Gestor de trazas para registrar logs, errores y evento
Downloads
331
Readme
@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
traceIdyspanId - ✅ 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
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:
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:
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:
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
TraceIdySpanId - 🔹 Niveles de severidad:
DEBUG,INFO,WARN,ERROR,FATAL - 🔹 Atributos personalizados en cada log
- 🔹 Compatible con formato OpenTelemetry para ClickHouse
Uso:
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:
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:
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:
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):
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
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
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
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:
http-create-order(root, SPAN_KIND_SERVER)OrderService.create(child, SPAN_KIND_INTERNAL)validate-order-rules(grandchild, SPAN_KIND_INTERNAL)
Y múltiples logs correlacionados con el mismo TraceId.
📝 Buenas Prácticas
- Usar
@Trace()en capas de negocio críticas: Controllers, Services, Repositories - Logger en puntos de decisión: Validaciones, llamadas externas, errores
- Atributos significativos: Agregar IDs de entidades, estados, providers
- Nombres descriptivos: Usar
nameen@Trace()para operaciones complejas - Metadata approach: El decorador captura metadata (tipos, cantidad) en lugar de valores completos
- 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:
import { addArgsMetadata } from '@hemia/trace-manager';
const args = ['[email protected]', 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:
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):
import { addRequestBodyMetadata } from '@hemia/trace-manager';
const reqBody = { email: '[email protected]', 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:
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:
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
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 AsyncLocalStorageuuid(^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
