@hemia/trace-manager 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +313 -60
- package/dist/hemia-trace-manager.esm.js +1 -1
- package/dist/hemia-trace-manager.js +1 -1
- package/dist/types/decorator/Traceable.d.ts +6 -4
- package/dist/types/index.d.ts +3 -5
- package/dist/types/log/Logger.d.ts +15 -0
- package/dist/types/types/traceOptions.d.ts +5 -18
- package/package.json +2 -3
- package/dist/types/TraceManager.d.ts +0 -50
- package/dist/types/types/traceModel.d.ts +0 -63
- package/dist/types/types/traceSchema.d.ts +0 -11
package/README.md
CHANGED
|
@@ -1,117 +1,370 @@
|
|
|
1
1
|
# @hemia/trace-manager
|
|
2
2
|
|
|
3
|
-
Sistema de
|
|
3
|
+
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.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## 🚀 Características
|
|
8
|
+
|
|
9
|
+
- ✅ **Decorador `@Trace`** - Instrumentación automática de métodos con captura de inputs/outputs
|
|
10
|
+
- ✅ **AsyncLocalStorage** - Propagación automática de contexto sin pasar parámetros manualmente
|
|
11
|
+
- ✅ **Logger Contextual** - Logs automáticamente correlacionados con `traceId` y `spanId`
|
|
12
|
+
- ✅ **Formato OpenTelemetry** - Compatible con estándares de observabilidad
|
|
13
|
+
- ✅ **ClickHouse Ready** - Esquema optimizado para análisis en ClickHouse
|
|
14
|
+
- ✅ **Captura de Errores** - Excepciones registradas como eventos en spans
|
|
15
|
+
- ✅ **Jerarquía de Spans** - Parent-Child spans automáticos por nivel de invocación
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 📦 Instalación
|
|
8
20
|
|
|
9
21
|
```bash
|
|
10
|
-
npm install @hemia/trace-manager
|
|
22
|
+
npm install @hemia/trace-manager @hemia/app-context
|
|
11
23
|
```
|
|
12
24
|
|
|
13
25
|
---
|
|
14
26
|
|
|
15
|
-
## Componentes principales
|
|
27
|
+
## 🎯 Componentes principales
|
|
28
|
+
|
|
29
|
+
### ✅ Decorador `@Trace()`
|
|
30
|
+
|
|
31
|
+
Instrumenta automáticamente métodos creando spans de OpenTelemetry con captura de inputs, outputs y errores.
|
|
32
|
+
|
|
33
|
+
#### **Características:**
|
|
34
|
+
|
|
35
|
+
- 🔹 Crea span automáticamente con `SpanId` único
|
|
36
|
+
- 🔹 Captura argumentos de entrada en `SpanAttributes.app.method.args`
|
|
37
|
+
- 🔹 Captura resultado de retorno en `SpanAttributes.app.method.result`
|
|
38
|
+
- 🔹 Registra excepciones como eventos (`EventsNested`)
|
|
39
|
+
- 🔹 Calcula duración en nanosegundos (`DurationUInt64`)
|
|
40
|
+
- 🔹 Propaga contexto automáticamente a métodos hijos
|
|
41
|
+
|
|
42
|
+
#### **Uso básico:**
|
|
16
43
|
|
|
17
|
-
|
|
44
|
+
```ts
|
|
45
|
+
import { Trace } from '@hemia/trace-manager';
|
|
46
|
+
|
|
47
|
+
class UserService {
|
|
48
|
+
@Trace()
|
|
49
|
+
async createUser(userData: any) {
|
|
50
|
+
// El decorador captura automáticamente:
|
|
51
|
+
// - Input: userData
|
|
52
|
+
// - Output: usuario creado
|
|
53
|
+
// - Duración de ejecución
|
|
54
|
+
// - Excepciones si ocurren
|
|
55
|
+
|
|
56
|
+
const user = await this.repository.save(userData);
|
|
57
|
+
return user;
|
|
58
|
+
}
|
|
18
59
|
|
|
19
|
-
|
|
60
|
+
@Trace({ name: 'validate-user-email' })
|
|
61
|
+
private async validateEmail(email: string) {
|
|
62
|
+
// Span hijo automático (hereda ParentSpanId)
|
|
63
|
+
return await this.emailValidator.check(email);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
20
67
|
|
|
21
|
-
####
|
|
68
|
+
#### **Opciones de configuración:**
|
|
22
69
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
70
|
+
```ts
|
|
71
|
+
interface TraceOptions {
|
|
72
|
+
name?: string; // Nombre personalizado del span (default: ClassName.methodName)
|
|
73
|
+
kind?: 'SPAN_KIND_INTERNAL' // Tipo de span: INTERNAL, SERVER, CLIENT, PRODUCER, CONSUMER
|
|
74
|
+
| 'SPAN_KIND_SERVER'
|
|
75
|
+
| 'SPAN_KIND_CLIENT'
|
|
76
|
+
| 'SPAN_KIND_PRODUCER'
|
|
77
|
+
| 'SPAN_KIND_CONSUMER';
|
|
78
|
+
attributes?: Record<string, string>; // Atributos personalizados
|
|
79
|
+
}
|
|
80
|
+
```
|
|
30
81
|
|
|
31
|
-
#### Ejemplo
|
|
82
|
+
#### **Ejemplo avanzado:**
|
|
32
83
|
|
|
33
84
|
```ts
|
|
34
|
-
|
|
35
|
-
|
|
85
|
+
class PaymentService {
|
|
86
|
+
@Trace({
|
|
87
|
+
name: 'process-payment-stripe',
|
|
88
|
+
kind: 'SPAN_KIND_CLIENT',
|
|
89
|
+
attributes: { 'payment.provider': 'stripe' }
|
|
90
|
+
})
|
|
91
|
+
async processPayment(amount: number, currency: string) {
|
|
92
|
+
// Span con atributos personalizados
|
|
93
|
+
return await this.stripeClient.charge(amount, currency);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
36
96
|
```
|
|
37
97
|
|
|
38
98
|
---
|
|
39
99
|
|
|
40
|
-
### ✅
|
|
100
|
+
### ✅ Logger Contextual
|
|
41
101
|
|
|
42
|
-
|
|
102
|
+
Logger que automáticamente correlaciona logs con el `traceId` y `spanId` activo, permitiendo rastrear logs específicos dentro de una traza distribuida.
|
|
43
103
|
|
|
44
|
-
####
|
|
104
|
+
#### **Características:**
|
|
45
105
|
|
|
46
|
-
|
|
47
|
-
|
|
106
|
+
- 🔹 Correlación automática con `TraceId` y `SpanId`
|
|
107
|
+
- 🔹 Niveles de severidad: `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`
|
|
108
|
+
- 🔹 Atributos personalizados en cada log
|
|
109
|
+
- 🔹 Compatible con formato OpenTelemetry para ClickHouse
|
|
110
|
+
|
|
111
|
+
#### **Uso:**
|
|
48
112
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
113
|
+
```ts
|
|
114
|
+
import { logger } from '@hemia/trace-manager';
|
|
115
|
+
|
|
116
|
+
class OrderService {
|
|
117
|
+
@Trace()
|
|
118
|
+
async placeOrder(order: Order) {
|
|
119
|
+
logger.info('Processing order', { orderId: order.id, amount: order.total });
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const result = await this.payment.charge(order.total);
|
|
123
|
+
logger.info('Payment successful', { transactionId: result.id });
|
|
124
|
+
return result;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
logger.error('Payment failed', { error: error.message });
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
54
129
|
}
|
|
55
130
|
}
|
|
56
131
|
```
|
|
57
132
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
133
|
+
#### **Métodos disponibles:**
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
logger.debug(message: string, attributes?: Record<string, any>): void
|
|
137
|
+
logger.info(message: string, attributes?: Record<string, any>): void
|
|
138
|
+
logger.warn(message: string, attributes?: Record<string, any>): void
|
|
139
|
+
logger.error(message: string, attributes?: Record<string, any>): void
|
|
140
|
+
logger.fatal(message: string, attributes?: Record<string, any>): void
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## 🏗️ Estructura de Datos
|
|
146
|
+
|
|
147
|
+
### **TraceSpan** (Span de OpenTelemetry)
|
|
148
|
+
|
|
149
|
+
Mapea directamente con la tabla `otel_traces` de ClickHouse:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
interface TraceSpan {
|
|
153
|
+
Timestamp: string; // ISO 8601 - DateTime64(9)
|
|
154
|
+
TraceId: string; // ID único de la traza
|
|
155
|
+
SpanId: string; // ID único del span
|
|
156
|
+
ParentSpanId: string; // ID del span padre (jerarquía)
|
|
157
|
+
TraceState: string; // Estado de propagación W3C
|
|
158
|
+
ServiceName: string; // Nombre del servicio
|
|
159
|
+
SpanName: string; // Ej: "UserService.createUser"
|
|
160
|
+
SpanKind: 'SPAN_KIND_INTERNAL' | 'SPAN_KIND_SERVER' | 'SPAN_KIND_CLIENT' | ...;
|
|
161
|
+
DurationUInt64: bigint; // Duración en nanosegundos
|
|
162
|
+
StatusCode: 'STATUS_CODE_OK' | 'STATUS_CODE_ERROR' | 'STATUS_CODE_UNSET';
|
|
163
|
+
StatusMessage: string;
|
|
164
|
+
SpanAttributes: Record<string, string>; // app.method.args, app.method.result, etc.
|
|
165
|
+
ResourceAttributes: Record<string, string>; // host.name, service.name, etc.
|
|
166
|
+
EventsNested: SpanEvent[]; // Excepciones, logs puntuales
|
|
167
|
+
LinksNested: SpanLink[]; // Enlaces a otras trazas
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### **TraceLog** (Log de OpenTelemetry)
|
|
172
|
+
|
|
173
|
+
Mapea directamente con la tabla `otel_logs` de ClickHouse:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
interface TraceLog {
|
|
177
|
+
Timestamp: string; // ISO 8601
|
|
178
|
+
TraceId: string; // Correlación automática con span activo
|
|
179
|
+
SpanId: string; // Span donde ocurrió el log
|
|
180
|
+
TraceFlags: number; // 1 = sampled
|
|
181
|
+
SeverityText: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'FATAL';
|
|
182
|
+
SeverityNumber: number; // 5, 9, 13, 17, 21
|
|
183
|
+
ServiceName: string;
|
|
184
|
+
Body: string; // Mensaje del log
|
|
185
|
+
LogAttributes: Record<string, string>; // Atributos custom
|
|
186
|
+
ResourceAttributes: Record<string, string>; // Metadata del servicio
|
|
187
|
+
}
|
|
188
|
+
```
|
|
61
189
|
|
|
62
190
|
---
|
|
63
191
|
|
|
64
|
-
|
|
192
|
+
## 🔗 Integración con `@hemia/app-context`
|
|
193
|
+
|
|
194
|
+
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.
|
|
65
195
|
|
|
66
|
-
|
|
196
|
+
### **Requisitos:**
|
|
197
|
+
|
|
198
|
+
El contexto debe ser inicializado previamente usando `@hemia/app-context` (generalmente en un middleware HTTP):
|
|
67
199
|
|
|
68
200
|
```ts
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
201
|
+
import { asyncContext } from '@hemia/app-context';
|
|
202
|
+
|
|
203
|
+
// En tu middleware o inicialización
|
|
204
|
+
const context = {
|
|
205
|
+
traceId: generateTraceId(),
|
|
206
|
+
spanId: generateSpanId(),
|
|
207
|
+
serviceName: 'user-service',
|
|
208
|
+
traceContext: {
|
|
209
|
+
resourceAttributes: {
|
|
210
|
+
'service.name': 'user-service',
|
|
211
|
+
'host.name': os.hostname(),
|
|
212
|
+
},
|
|
213
|
+
spans: [],
|
|
214
|
+
logs: []
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
await asyncContext.run(context, async () => {
|
|
219
|
+
// Todo el código dentro hereda este contexto automáticamente
|
|
220
|
+
await handleRequest(req, res);
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## 📊 Esquema ClickHouse
|
|
227
|
+
|
|
228
|
+
### Tabla: `otel_traces`
|
|
229
|
+
|
|
230
|
+
```sql
|
|
231
|
+
CREATE TABLE otel_traces (
|
|
232
|
+
Timestamp DateTime64(9) CODEC(Delta, ZSTD(1)),
|
|
233
|
+
TraceId String CODEC(ZSTD(1)),
|
|
234
|
+
SpanId String CODEC(ZSTD(1)),
|
|
235
|
+
ParentSpanId String CODEC(ZSTD(1)),
|
|
236
|
+
ServiceName LowCardinality(String) CODEC(ZSTD(1)),
|
|
237
|
+
SpanName LowCardinality(String) CODEC(ZSTD(1)),
|
|
238
|
+
SpanKind LowCardinality(String) CODEC(ZSTD(1)),
|
|
239
|
+
DurationUInt64 UInt64 CODEC(ZSTD(1)),
|
|
240
|
+
StatusCode LowCardinality(String) CODEC(ZSTD(1)),
|
|
241
|
+
SpanAttributes Map(LowCardinality(String), String) CODEC(ZSTD(1)),
|
|
242
|
+
ResourceAttributes Map(LowCardinality(String), String) CODEC(ZSTD(1)),
|
|
243
|
+
EventsNested Nested (
|
|
244
|
+
Timestamp DateTime64(9),
|
|
245
|
+
Name LowCardinality(String),
|
|
246
|
+
Attributes Map(LowCardinality(String), String)
|
|
247
|
+
) CODEC(ZSTD(1))
|
|
248
|
+
) ENGINE = MergeTree()
|
|
249
|
+
PARTITION BY toDate(Timestamp)
|
|
250
|
+
ORDER BY (ServiceName, SpanName, toUnixTimestamp(Timestamp), TraceId);
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Tabla: `otel_logs`
|
|
254
|
+
|
|
255
|
+
```sql
|
|
256
|
+
CREATE TABLE otel_logs (
|
|
257
|
+
Timestamp DateTime64(9) CODEC(Delta, ZSTD(1)),
|
|
258
|
+
TraceId String CODEC(ZSTD(1)),
|
|
259
|
+
SpanId String CODEC(ZSTD(1)),
|
|
260
|
+
SeverityText LowCardinality(String) CODEC(ZSTD(1)),
|
|
261
|
+
SeverityNumber Int32 CODEC(ZSTD(1)),
|
|
262
|
+
ServiceName LowCardinality(String) CODEC(ZSTD(1)),
|
|
263
|
+
Body String CODEC(ZSTD(1)),
|
|
264
|
+
LogAttributes Map(LowCardinality(String), String) CODEC(ZSTD(1)),
|
|
265
|
+
ResourceAttributes Map(LowCardinality(String), String) CODEC(ZSTD(1))
|
|
266
|
+
) ENGINE = MergeTree()
|
|
267
|
+
PARTITION BY toDate(Timestamp)
|
|
268
|
+
ORDER BY (ServiceName, SeverityText, toUnixTimestamp(Timestamp), TraceId);
|
|
72
269
|
```
|
|
73
270
|
|
|
74
271
|
---
|
|
75
272
|
|
|
76
|
-
##
|
|
273
|
+
## 🔍 Ejemplo completo
|
|
77
274
|
|
|
78
275
|
```ts
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
276
|
+
import { Trace, logger } from '@hemia/trace-manager';
|
|
277
|
+
import { asyncContext } from '@hemia/app-context';
|
|
278
|
+
|
|
279
|
+
class OrderController {
|
|
280
|
+
@Trace({ name: 'http-create-order', kind: 'SPAN_KIND_SERVER' })
|
|
281
|
+
async createOrder(req: Request) {
|
|
282
|
+
logger.info('Order request received', { userId: req.userId });
|
|
283
|
+
|
|
284
|
+
const order = await this.orderService.create(req.body);
|
|
285
|
+
|
|
286
|
+
logger.info('Order created successfully', { orderId: order.id });
|
|
287
|
+
return order;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
class OrderService {
|
|
292
|
+
@Trace()
|
|
293
|
+
async create(orderData: any) {
|
|
294
|
+
// Span hijo automático (ParentSpanId = span del controller)
|
|
295
|
+
logger.debug('Validating order data');
|
|
296
|
+
|
|
297
|
+
await this.validate(orderData);
|
|
298
|
+
const order = await this.repository.save(orderData);
|
|
299
|
+
|
|
300
|
+
logger.info('Order persisted', { orderId: order.id });
|
|
301
|
+
return order;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
@Trace({ name: 'validate-order-rules' })
|
|
305
|
+
private async validate(data: any) {
|
|
306
|
+
// Span nieto (hijo del método create)
|
|
307
|
+
if (!data.items?.length) {
|
|
308
|
+
logger.error('Validation failed: no items');
|
|
309
|
+
throw new Error('Order must have items');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
97
312
|
}
|
|
98
313
|
```
|
|
99
314
|
|
|
315
|
+
**Resultado en ClickHouse:**
|
|
316
|
+
|
|
317
|
+
3 spans jerárquicos:
|
|
318
|
+
1. `http-create-order` (root, SPAN_KIND_SERVER)
|
|
319
|
+
2. `OrderService.create` (child, SPAN_KIND_INTERNAL)
|
|
320
|
+
3. `validate-order-rules` (grandchild, SPAN_KIND_INTERNAL)
|
|
321
|
+
|
|
322
|
+
Y múltiples logs correlacionados con el mismo `TraceId`.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## 📝 Buenas Prácticas
|
|
327
|
+
|
|
328
|
+
1. **Usar `@Trace()` en capas de negocio críticas**: Controllers, Services, Repositories
|
|
329
|
+
2. **Logger en puntos de decisión**: Validaciones, llamadas externas, errores
|
|
330
|
+
3. **Atributos significativos**: Agregar IDs de entidades, estados, providers
|
|
331
|
+
4. **Nombres descriptivos**: Usar `name` en `@Trace()` para operaciones complejas
|
|
332
|
+
5. **Sanitización automática**: Los inputs/outputs se sanitizan para evitar datos sensibles
|
|
333
|
+
|
|
100
334
|
---
|
|
101
335
|
|
|
102
|
-
##
|
|
336
|
+
## 🛠️ Utilidades
|
|
103
337
|
|
|
104
|
-
|
|
105
|
-
|
|
338
|
+
### `sanitizeArgs()` y `toSafeJSON()`
|
|
339
|
+
|
|
340
|
+
Utilidades internas que previenen:
|
|
341
|
+
- Serialización de objetos circulares
|
|
342
|
+
- Exposición de contraseñas/tokens en atributos
|
|
343
|
+
- Overflow de tamaño en ClickHouse
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## 📚 Dependencias
|
|
348
|
+
|
|
349
|
+
- `@hemia/app-context` - Manejo de contexto con AsyncLocalStorage
|
|
350
|
+
- `uuid` - Generación de IDs únicos para spans
|
|
106
351
|
|
|
107
352
|
---
|
|
108
353
|
|
|
109
|
-
## Licencia
|
|
354
|
+
## 📄 Licencia
|
|
110
355
|
|
|
111
356
|
MIT
|
|
112
357
|
|
|
113
358
|
---
|
|
114
359
|
|
|
115
|
-
## Autor
|
|
360
|
+
## 👨💻 Autor
|
|
361
|
+
|
|
362
|
+
**Hemia Technologies**
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## 🔗 Recursos
|
|
116
367
|
|
|
117
|
-
|
|
368
|
+
- [OpenTelemetry Specification](https://opentelemetry.io/docs/specs/otel/)
|
|
369
|
+
- [ClickHouse Documentation](https://clickhouse.com/docs)
|
|
370
|
+
- [AsyncLocalStorage Node.js](https://nodejs.org/api/async_context.html)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{asyncContext as t}from"@hemia/app-context";
|
|
1
|
+
import{asyncContext as t}from"@hemia/app-context";var e;!function(t){t.TRACE="x-trace-id",t.CORRELATION="x-correlation-id"}(e||(e={}));const n={DEBUG:5,INFO:9,WARN:13,ERROR:17,FATAL:21};const r=new class{constructor(t){this.defaultServiceName=t}log(e,r,o){const s=t.getStore();if(!s)return console.warn("[Logger] No active context. Log not traced."),void console.log(`[${e}] ${r}`,o);const a={Timestamp:(new Date).toISOString(),TraceId:s.traceId,SpanId:s.spanId,TraceFlags:1,SeverityText:e,SeverityNumber:n[e],ServiceName:s.serviceName,Body:r,LogAttributes:this.stringifyAttributes(o||{}),ResourceAttributes:s.traceContext.resourceAttributes};s.traceContext.logs.push(a),"production"!==process.env.NODE_ENV&&console.log(`[${e}] [TraceId:${s.traceId.substring(0,8)}...] ${r}`,o||"")}stringifyAttributes(t){const e={};for(const[n,r]of Object.entries(t))if(null==r)e[n]="";else if("string"==typeof r)e[n]=r;else try{e[n]=JSON.stringify(r)}catch{e[n]=String(r)}return e}debug(t,e){this.log("DEBUG",t,e)}info(t,e){this.log("INFO",t,e)}warn(t,e){this.log("WARN",t,e)}error(t,e){this.log("ERROR",t,e)}fatal(t,e){this.log("FATAL",t,e)}};function o(t){try{const e=JSON.stringify(t,function(){const t=new WeakSet;return(e,n)=>{if(null===n)return null;if("object"==typeof n){if(t.has(n))return"[Circular]";t.add(n)}return"function"==typeof n?n.toString():n}}());return JSON.parse(e)}catch(e){return String(t)}}const s=["content-type","host","x-no-cookies","x-api-key","Authorization","origin"];function a(t){return t.map(t=>{if(function(t){return null!=t&&"object"==typeof t&&"string"==typeof t.method&&"string"==typeof t.url&&"object"==typeof t.headers}(t)){const e=s.reduce((e,n)=>{const r=n.toLowerCase();return t.headers[r]&&(e[r]=t.headers[r]),e},{});return{method:t.method,url:t.url,params:t.params,query:t.query,body:t.body,headers:e}}return function(t){return null!=t&&"object"==typeof t&&"number"==typeof t.statusCode&&"function"==typeof t.setHeader&&"function"==typeof t.end}(t)?"[ExpressResponseObject]":"function"==typeof t?"[Function next]":t})}for(var i,c=[],u=0;u<256;++u)c.push((u+256).toString(16).slice(1));var p=new Uint8Array(16);function d(){if(!i&&!(i="undefined"!=typeof crypto&&crypto.getRandomValues&&crypto.getRandomValues.bind(crypto)))throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");return i(p)}var f={randomUUID:"undefined"!=typeof crypto&&crypto.randomUUID&&crypto.randomUUID.bind(crypto)};function g(t,e,n){if(f.randomUUID&&!t)return f.randomUUID();var r=(t=t||{}).random||(t.rng||d)();return r[6]=15&r[6]|64,r[8]=63&r[8]|128,function(t,e=0){return(c[t[e+0]]+c[t[e+1]]+c[t[e+2]]+c[t[e+3]]+"-"+c[t[e+4]]+c[t[e+5]]+"-"+c[t[e+6]]+c[t[e+7]]+"-"+c[t[e+8]]+c[t[e+9]]+"-"+c[t[e+10]]+c[t[e+11]]+c[t[e+12]]+c[t[e+13]]+c[t[e+14]]+c[t[e+15]]).toLowerCase()}(r)}function l(e){return function(n,r,s){const i=s?.value;if("function"!=typeof i)throw new Error("@Trace can only be applied to methods");s.value=async function(...s){const c=t.getStore();if(!c)return console.warn("[Trace] No active context. Skipping trace."),await i.apply(this,s);const u=g(),p=this?.constructor?.name||n.name||"UnknownClass",d=e?.name||`${p}.${r}`,f=process.hrtime.bigint(),l=(new Date).toISOString(),m=c.spanId,y={Timestamp:l,TraceId:c.traceId,SpanId:u,ParentSpanId:m,TraceState:"",ServiceName:c.serviceName,SpanName:d,SpanKind:e?.kind||"SPAN_KIND_INTERNAL",DurationUInt64:BigInt(0),StatusCode:"STATUS_CODE_UNSET",StatusMessage:"",SpanAttributes:{"code.function":r,"code.namespace":p,...e?.attributes},ResourceAttributes:c.traceContext.resourceAttributes,EventsNested:[],LinksNested:[]},S=c.spanId;c.spanId=u;try{const t=a(s);y.SpanAttributes["app.method.args"]=o(t),y.SpanAttributes["app.method.args.count"]=s.length.toString();const e=await i.apply(this,s),n=o(a(Array.isArray(e)?e:[e]));return y.SpanAttributes["app.method.result"]=n.substring(0,2e3),y.StatusCode="STATUS_CODE_OK",e}catch(t){const e={Timestamp:(new Date).toISOString(),Name:"exception",Attributes:{"exception.type":t.constructor?.name||"Error","exception.message":t.message||"Unknown error","exception.stacktrace":(t.stack||"").substring(0,5e3)}};throw y.EventsNested.push(e),y.StatusCode="STATUS_CODE_ERROR",y.StatusMessage=t.message||"Unhandled exception",t}finally{const t=process.hrtime.bigint();y.DurationUInt64=t-f,c.traceContext.spans.push(y),c.spanId=S}}}}export{l as Trace,e as TraceHeader,r as logger};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var t,e=require("@hemia/app-context"),
|
|
1
|
+
"use strict";var t,e=require("@hemia/app-context");exports.TraceHeader=void 0,(t=exports.TraceHeader||(exports.TraceHeader={})).TRACE="x-trace-id",t.CORRELATION="x-correlation-id";const r={DEBUG:5,INFO:9,WARN:13,ERROR:17,FATAL:21};const n=new class{constructor(t){this.defaultServiceName=t}log(t,n,o){const s=e.asyncContext.getStore();if(!s)return console.warn("[Logger] No active context. Log not traced."),void console.log(`[${t}] ${n}`,o);const a={Timestamp:(new Date).toISOString(),TraceId:s.traceId,SpanId:s.spanId,TraceFlags:1,SeverityText:t,SeverityNumber:r[t],ServiceName:s.serviceName,Body:n,LogAttributes:this.stringifyAttributes(o||{}),ResourceAttributes:s.traceContext.resourceAttributes};s.traceContext.logs.push(a),"production"!==process.env.NODE_ENV&&console.log(`[${t}] [TraceId:${s.traceId.substring(0,8)}...] ${n}`,o||"")}stringifyAttributes(t){const e={};for(const[r,n]of Object.entries(t))if(null==n)e[r]="";else if("string"==typeof n)e[r]=n;else try{e[r]=JSON.stringify(n)}catch{e[r]=String(n)}return e}debug(t,e){this.log("DEBUG",t,e)}info(t,e){this.log("INFO",t,e)}warn(t,e){this.log("WARN",t,e)}error(t,e){this.log("ERROR",t,e)}fatal(t,e){this.log("FATAL",t,e)}};function o(t){try{const e=JSON.stringify(t,function(){const t=new WeakSet;return(e,r)=>{if(null===r)return null;if("object"==typeof r){if(t.has(r))return"[Circular]";t.add(r)}return"function"==typeof r?r.toString():r}}());return JSON.parse(e)}catch(e){return String(t)}}const s=["content-type","host","x-no-cookies","x-api-key","Authorization","origin"];function a(t){return t.map(t=>{if(function(t){return null!=t&&"object"==typeof t&&"string"==typeof t.method&&"string"==typeof t.url&&"object"==typeof t.headers}(t)){const e=s.reduce((e,r)=>{const n=r.toLowerCase();return t.headers[n]&&(e[n]=t.headers[n]),e},{});return{method:t.method,url:t.url,params:t.params,query:t.query,body:t.body,headers:e}}return function(t){return null!=t&&"object"==typeof t&&"number"==typeof t.statusCode&&"function"==typeof t.setHeader&&"function"==typeof t.end}(t)?"[ExpressResponseObject]":"function"==typeof t?"[Function next]":t})}for(var i,c=[],u=0;u<256;++u)c.push((u+256).toString(16).slice(1));var p=new Uint8Array(16);function d(){if(!i&&!(i="undefined"!=typeof crypto&&crypto.getRandomValues&&crypto.getRandomValues.bind(crypto)))throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");return i(p)}var f={randomUUID:"undefined"!=typeof crypto&&crypto.randomUUID&&crypto.randomUUID.bind(crypto)};function g(t,e,r){if(f.randomUUID&&!t)return f.randomUUID();var n=(t=t||{}).random||(t.rng||d)();return n[6]=15&n[6]|64,n[8]=63&n[8]|128,function(t,e=0){return(c[t[e+0]]+c[t[e+1]]+c[t[e+2]]+c[t[e+3]]+"-"+c[t[e+4]]+c[t[e+5]]+"-"+c[t[e+6]]+c[t[e+7]]+"-"+c[t[e+8]]+c[t[e+9]]+"-"+c[t[e+10]]+c[t[e+11]]+c[t[e+12]]+c[t[e+13]]+c[t[e+14]]+c[t[e+15]]).toLowerCase()}(n)}exports.Trace=function(t){return function(r,n,s){const i=s?.value;if("function"!=typeof i)throw new Error("@Trace can only be applied to methods");s.value=async function(...s){const c=e.asyncContext.getStore();if(!c)return console.warn("[Trace] No active context. Skipping trace."),await i.apply(this,s);const u=g(),p=this?.constructor?.name||r.name||"UnknownClass",d=t?.name||`${p}.${n}`,f=process.hrtime.bigint(),l=(new Date).toISOString(),y=c.spanId,m={Timestamp:l,TraceId:c.traceId,SpanId:u,ParentSpanId:y,TraceState:"",ServiceName:c.serviceName,SpanName:d,SpanKind:t?.kind||"SPAN_KIND_INTERNAL",DurationUInt64:BigInt(0),StatusCode:"STATUS_CODE_UNSET",StatusMessage:"",SpanAttributes:{"code.function":n,"code.namespace":p,...t?.attributes},ResourceAttributes:c.traceContext.resourceAttributes,EventsNested:[],LinksNested:[]},S=c.spanId;c.spanId=u;try{const t=a(s);m.SpanAttributes["app.method.args"]=o(t),m.SpanAttributes["app.method.args.count"]=s.length.toString();const e=await i.apply(this,s),r=o(a(Array.isArray(e)?e:[e]));return m.SpanAttributes["app.method.result"]=r.substring(0,2e3),m.StatusCode="STATUS_CODE_OK",e}catch(t){const e={Timestamp:(new Date).toISOString(),Name:"exception",Attributes:{"exception.type":t.constructor?.name||"Error","exception.message":t.message||"Unknown error","exception.stacktrace":(t.stack||"").substring(0,5e3)}};throw m.EventsNested.push(e),m.StatusCode="STATUS_CODE_ERROR",m.StatusMessage=t.message||"Unhandled exception",t}finally{const t=process.hrtime.bigint();m.DurationUInt64=t-f,c.traceContext.spans.push(m),c.spanId=S}}}},exports.logger=n;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { TraceOptions } from "../types/traceOptions";
|
|
2
|
+
/**
|
|
3
|
+
* Decorador que captura método como Span en formato OpenTelemetry/ClickHouse
|
|
4
|
+
* Usa AsyncLocalStorage para propagar contexto automáticamente
|
|
5
|
+
*/
|
|
6
|
+
export declare function Trace(options?: TraceOptions): <T extends (...args: any[]) => any>(target: any, key: string, descriptor: PropertyDescriptor) => void;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
export { TraceHeader } from "./types/traceHeadersNames";
|
|
2
2
|
export { TraceContext } from "./types/traceContext";
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export { Trace, ITrace } from "./types/traceModel";
|
|
7
|
-
export { TraceSchema } from "./types/traceSchema";
|
|
3
|
+
export { TraceOptions } from "./types/traceOptions";
|
|
4
|
+
export { logger } from "./log/Logger";
|
|
5
|
+
export { Trace } from "./decorator/Traceable";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare class Logger {
|
|
2
|
+
private defaultServiceName?;
|
|
3
|
+
constructor(defaultServiceName?: string | undefined);
|
|
4
|
+
/**
|
|
5
|
+
* Logs que se correlacionan automáticamente con el TraceId activo
|
|
6
|
+
*/
|
|
7
|
+
private log;
|
|
8
|
+
private stringifyAttributes;
|
|
9
|
+
debug(message: string, attributes?: Record<string, any>): void;
|
|
10
|
+
info(message: string, attributes?: Record<string, any>): void;
|
|
11
|
+
warn(message: string, attributes?: Record<string, any>): void;
|
|
12
|
+
error(message: string, attributes?: Record<string, any>): void;
|
|
13
|
+
fatal(message: string, attributes?: Record<string, any>): void;
|
|
14
|
+
}
|
|
15
|
+
export declare const logger: Logger;
|
|
@@ -1,19 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* @property {string} st - Estado inicial o mensaje que indica el inicio del trazo.
|
|
7
|
-
* @property {string} [errSt] - Estado o mensaje que describe el error (opcional).
|
|
8
|
-
* @property {string} [errCode] - Código que identifica el tipo de error (opcional).
|
|
9
|
-
*/
|
|
10
|
-
export interface ITraceOptions {
|
|
11
|
-
ck?: string;
|
|
12
|
-
fn?: string;
|
|
13
|
-
st?: string;
|
|
14
|
-
errSt?: string;
|
|
15
|
-
errCode?: string;
|
|
16
|
-
description?: string;
|
|
17
|
-
tags?: string[];
|
|
18
|
-
source?: 'api' | 'web' | 'mobile' | 'cron' | 'system' | string;
|
|
1
|
+
import { TraceSpan } from "@hemia/app-context";
|
|
2
|
+
export interface TraceOptions {
|
|
3
|
+
name?: string;
|
|
4
|
+
kind?: TraceSpan['SpanKind'];
|
|
5
|
+
attributes?: Record<string, string>;
|
|
19
6
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hemia/trace-manager",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Gestor de trazas para registrar logs, errores y evento",
|
|
5
5
|
"main": "dist/hemia-trace-manager.js",
|
|
6
6
|
"module": "dist/hemia-trace-manager.esm.js",
|
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
"test": "jest --passWithNoTests --detectOpenHandles"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@hemia/app-context": "^0.0.
|
|
17
|
-
"@hemia/db-manager": "^0.0.1",
|
|
16
|
+
"@hemia/app-context": "^0.0.3",
|
|
18
17
|
"@rollup/plugin-commonjs": "^26.0.1",
|
|
19
18
|
"@rollup/plugin-json": "^6.1.0",
|
|
20
19
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { AggregateOptions, ClientSession, NoSQLOptions, NoSQLRepository, PipelineStage, FilterQuery, Document } from "@hemia/db-manager";
|
|
2
|
-
export declare class TraceManager<T extends Document> {
|
|
3
|
-
private repository;
|
|
4
|
-
constructor(service: NoSQLRepository<T>);
|
|
5
|
-
/**
|
|
6
|
-
* Servicio que permite guardar LOGS
|
|
7
|
-
* @param level - Nivel del Log
|
|
8
|
-
* @param message - Mensaje
|
|
9
|
-
* @param traceId - Trace ID
|
|
10
|
-
* @param trace - Traza
|
|
11
|
-
* @param additionalData - Datos adicionales
|
|
12
|
-
* @returns Documento guardado
|
|
13
|
-
*/
|
|
14
|
-
save(traceId: string, additionalData?: Partial<T>): Promise<T | null>;
|
|
15
|
-
/**
|
|
16
|
-
* Servicio que permite buscar documentos en la colección
|
|
17
|
-
* @param filter Filtros que se aplican en la colección
|
|
18
|
-
* @returns Lista de documentos de la colección
|
|
19
|
-
*/
|
|
20
|
-
find(filter: FilterQuery<T>, options?: NoSQLOptions): Promise<T[]>;
|
|
21
|
-
/**
|
|
22
|
-
* Servicio que permite buscar un documento en la colleción
|
|
23
|
-
* @param filter - Filtros de la colección
|
|
24
|
-
* @returns Documentos de la colección
|
|
25
|
-
*/
|
|
26
|
-
findOne(filter: FilterQuery<T>): Promise<T | null>;
|
|
27
|
-
/**
|
|
28
|
-
* Servicio que permite obtener un documento por ID
|
|
29
|
-
* @param id - Identificador del documento
|
|
30
|
-
* @returns Documento
|
|
31
|
-
*/
|
|
32
|
-
getById(id: string): Promise<T | null>;
|
|
33
|
-
/**
|
|
34
|
-
* Serivicio para actualizar documentos en la base de datos
|
|
35
|
-
* @param filter - criterio de actualización
|
|
36
|
-
* @param updateData - Datos del documento a actualizar
|
|
37
|
-
* @returns Documento actualizado
|
|
38
|
-
*/
|
|
39
|
-
update(filter: FilterQuery<T>, updateData: Partial<T>, session?: ClientSession): Promise<T | null>;
|
|
40
|
-
/**
|
|
41
|
-
* Servicio para eliminar un documento de la base de datos
|
|
42
|
-
* @param id - Identificador del documento
|
|
43
|
-
* @returns Documento Eliminado
|
|
44
|
-
*/
|
|
45
|
-
delete(filter: FilterQuery<T>, session?: ClientSession): Promise<void>;
|
|
46
|
-
/**
|
|
47
|
-
* Ejecuta una consulta de agregación con un pipeline personalizado.
|
|
48
|
-
*/
|
|
49
|
-
aggregate(pipeline: PipelineStage[], options?: AggregateOptions): Promise<any[]>;
|
|
50
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { Document } from "mongoose";
|
|
2
|
-
export interface ITrace extends Document {
|
|
3
|
-
traceId: string;
|
|
4
|
-
projectId: string;
|
|
5
|
-
meta?: TraceMetadata;
|
|
6
|
-
trace: Trace[];
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Node de las trazas
|
|
10
|
-
*/
|
|
11
|
-
export interface Trace {
|
|
12
|
-
description: string;
|
|
13
|
-
traceId: string;
|
|
14
|
-
spanId: string;
|
|
15
|
-
parentSpanId: string;
|
|
16
|
-
projectId: string;
|
|
17
|
-
parentId?: number;
|
|
18
|
-
checkpoint: string;
|
|
19
|
-
function: string;
|
|
20
|
-
status: boolean;
|
|
21
|
-
type: "input" | "output";
|
|
22
|
-
startTime: number;
|
|
23
|
-
endTime?: number;
|
|
24
|
-
duration?: number;
|
|
25
|
-
inputData?: any;
|
|
26
|
-
outputData?: any;
|
|
27
|
-
additionalData?: any;
|
|
28
|
-
tags?: string[];
|
|
29
|
-
error?: {
|
|
30
|
-
message: string;
|
|
31
|
-
code?: string;
|
|
32
|
-
stack?: string;
|
|
33
|
-
};
|
|
34
|
-
source?: 'api' | 'web' | 'mobile' | 'cron' | 'system' | string;
|
|
35
|
-
}
|
|
36
|
-
export interface TraceMetadata {
|
|
37
|
-
userAgent: string | null;
|
|
38
|
-
environment: string | null;
|
|
39
|
-
platform: string | null;
|
|
40
|
-
ip: string | null;
|
|
41
|
-
user: {
|
|
42
|
-
id: string;
|
|
43
|
-
email: string;
|
|
44
|
-
} | null;
|
|
45
|
-
event: string;
|
|
46
|
-
url: string;
|
|
47
|
-
path: string;
|
|
48
|
-
route: string | null;
|
|
49
|
-
tags: string[];
|
|
50
|
-
browser: {
|
|
51
|
-
name: string | null;
|
|
52
|
-
version: string | null;
|
|
53
|
-
};
|
|
54
|
-
os: {
|
|
55
|
-
name: string | null;
|
|
56
|
-
version: string | null;
|
|
57
|
-
};
|
|
58
|
-
device: {
|
|
59
|
-
model: string | null;
|
|
60
|
-
type: string | null;
|
|
61
|
-
vendor: string | null;
|
|
62
|
-
};
|
|
63
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { Schema } from "@hemia/db-manager";
|
|
2
|
-
import { ITrace } from "./traceModel";
|
|
3
|
-
export declare const TraceSchema: Schema<ITrace, import("mongoose").Model<ITrace, any, any, any, import("mongoose").Document<unknown, any, ITrace, any, {}> & ITrace & Required<{
|
|
4
|
-
_id: unknown;
|
|
5
|
-
}> & {
|
|
6
|
-
__v: number;
|
|
7
|
-
}, any>, {}, {}, {}, {}, import("mongoose").DefaultSchemaOptions, ITrace, import("mongoose").Document<unknown, {}, import("mongoose").FlatRecord<ITrace>, {}, import("mongoose").ResolveSchemaOptions<import("mongoose").DefaultSchemaOptions>> & import("mongoose").FlatRecord<ITrace> & Required<{
|
|
8
|
-
_id: unknown;
|
|
9
|
-
}> & {
|
|
10
|
-
__v: number;
|
|
11
|
-
}>;
|