Gestor de trazas para registrar logs, errores y evento
npm install @hemia/trace-managerSistema 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.
---
- ✅ 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
---
``bash`
npm install @hemia/trace-manager @hemia/app-context
---
Instrumenta automáticamente métodos creando spans de OpenTelemetry con captura de inputs, outputs y errores.
#### Características:
- 🔹 Crea span automáticamente con SpanId únicoEventsNested
- 🔹 Captura metadata de argumentos (tipos, cantidad) - no valores completos
- 🔹 Captura metadata de resultados (tipo, isArray, length) - no valores completos
- 🔹 Registra excepciones como eventos ()DurationUInt64
- 🔹 Calcula duración en nanosegundos ()
- 🔹 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
}
#### 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 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 SpanIdDEBUG
- 🔹 Niveles de severidad: , 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
logger.info(message: string, attributes?: Record
logger.warn(message: string, attributes?: Record
logger.error(message: string, attributes?: Record
logger.fatal(message: string, attributes?: Record
---
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
ResourceAttributes: Record
EventsNested: SpanEvent[]; // Excepciones, logs puntuales
LinksNested: SpanLink[]; // Enlaces a otras trazas
}
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
ResourceAttributes: Record
}
---
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.
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);
});
`
---
`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);
`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);
---
`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)OrderService.create
2. (child, SPAN_KIND_INTERNAL)validate-order-rules
3. (grandchild, SPAN_KIND_INTERNAL)
Y múltiples logs correlacionados con el mismo TraceId.
---
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
---
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"
// }
`
`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();
};
};
`
✅ 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
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.
---
- @hemia/app-context (^0.0.6) - Manejo de contexto con AsyncLocalStorageuuid` (^10.0.0) - Generación de IDs únicos para spans
-
v0.0.3 - Metadata approach implementado (Noviembre 2025)
---
MIT
---
Hemia Technologies
---
- OpenTelemetry Specification
- ClickHouse Documentation
- AsyncLocalStorage Node.js