@hemia/trace-manager 0.0.3 → 0.0.4

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 CHANGED
@@ -33,11 +33,12 @@ Instrumenta automáticamente métodos creando spans de OpenTelemetry con captura
33
33
  #### **Características:**
34
34
 
35
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`
36
+ - 🔹 Captura **metadata** de argumentos (tipos, cantidad) - no valores completos
37
+ - 🔹 Captura **metadata** de resultados (tipo, isArray, length) - no valores completos
38
38
  - 🔹 Registra excepciones como eventos (`EventsNested`)
39
39
  - 🔹 Calcula duración en nanosegundos (`DurationUInt64`)
40
40
  - 🔹 Propaga contexto automáticamente a métodos hijos
41
+ - 🔹 **Privacy-first**: No serializa valores sensibles, solo metadata
41
42
 
42
43
  #### **Uso básico:**
43
44
 
@@ -48,10 +49,10 @@ class UserService {
48
49
  @Trace()
49
50
  async createUser(userData: any) {
50
51
  // El decorador captura automáticamente:
51
- // - Input: userData
52
- // - Output: usuario creado
53
- // - Duración de ejecución
54
- // - Excepciones si ocurren
52
+ // - Metadata de input: tipo, cantidad de args (NO valores completos)
53
+ // - Metadata de output: tipo, isArray, length (NO valores completos)
54
+ // - Duración de ejecución en nanosegundos
55
+ // - Excepciones si ocurren (con stacktrace)
55
56
 
56
57
  const user = await this.repository.save(userData);
57
58
  return user;
@@ -329,25 +330,171 @@ Y múltiples logs correlacionados con el mismo `TraceId`.
329
330
  2. **Logger en puntos de decisión**: Validaciones, llamadas externas, errores
330
331
  3. **Atributos significativos**: Agregar IDs de entidades, estados, providers
331
332
  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
+ 5. **Metadata approach**: El decorador captura metadata (tipos, cantidad) en lugar de valores completos
334
+ 6. **Privacy by default**: No se serializan valores sensibles como passwords o tokens
333
335
 
334
336
  ---
335
337
 
336
338
  ## 🛠️ Utilidades
337
339
 
340
+ ### Helpers para SpanAttributes (Metadata Approach)
341
+
342
+ El paquete incluye utilidades para agregar metadata en lugar de serializar datos completos, siguiendo las mejores prácticas de observabilidad:
343
+
344
+ #### **`addArgsMetadata(attributes, args)`**
345
+ Agrega metadata de argumentos de función:
346
+ ```ts
347
+ import { addArgsMetadata } from '@hemia/trace-manager';
348
+
349
+ const args = ['user@test.com', 123, { name: 'John' }];
350
+ addArgsMetadata(span.SpanAttributes, args);
351
+
352
+ // Resultado:
353
+ // {
354
+ // "app.method.args.count": "3",
355
+ // "app.method.args.types": "string,number,object"
356
+ // }
357
+ ```
358
+
359
+ #### **`addResultMetadata(attributes, result)`**
360
+ Agrega metadata de resultado de función:
361
+ ```ts
362
+ import { addResultMetadata } from '@hemia/trace-manager';
363
+
364
+ const result = [{ id: 1 }, { id: 2 }];
365
+ addResultMetadata(span.SpanAttributes, result);
366
+
367
+ // Resultado:
368
+ // {
369
+ // "app.method.result.type": "object",
370
+ // "app.method.result.isArray": "true",
371
+ // "app.method.result.length": "2"
372
+ // }
373
+ ```
374
+
375
+ #### **`addRequestBodyMetadata(attributes, body, prefix?)`**
376
+ Agrega metadata de Request body (para middlewares HTTP):
377
+ ```ts
378
+ import { addRequestBodyMetadata } from '@hemia/trace-manager';
379
+
380
+ const reqBody = { email: 'user@test.com', password: '***' };
381
+ addRequestBodyMetadata(span.SpanAttributes, reqBody);
382
+
383
+ // Resultado:
384
+ // {
385
+ // "http.request.body.exists": "true",
386
+ // "http.request.body.type": "object",
387
+ // "http.request.body.keys": "email,password",
388
+ // "http.request.body.keyCount": "2"
389
+ // }
390
+ ```
391
+
392
+ #### **`addResponseBodyMetadata(attributes, body, isError, prefix?)`**
393
+ Agrega metadata de Response body con captura inteligente de errores:
394
+ ```ts
395
+ import { addResponseBodyMetadata } from '@hemia/trace-manager';
396
+
397
+ const errorBody = {
398
+ code: 'AUTH_ERROR',
399
+ message: 'Invalid credentials'
400
+ };
401
+
402
+ addResponseBodyMetadata(
403
+ span.SpanAttributes,
404
+ errorBody,
405
+ true // isError = true activa captura de mensaje
406
+ );
407
+
408
+ // Resultado:
409
+ // {
410
+ // "http.response.body.exists": "true",
411
+ // "http.response.body.type": "object",
412
+ // "http.response.body.keys": "code,message",
413
+ // "http.response.body.keyCount": "2",
414
+ // "http.response.error.code": "AUTH_ERROR",
415
+ // "http.response.error.message": "Invalid credentials"
416
+ // }
417
+ ```
418
+
419
+ #### **`addObjectMetadata(attributes, data, prefix)`**
420
+ Agrega metadata genérica de cualquier objeto:
421
+ ```ts
422
+ import { addObjectMetadata } from '@hemia/trace-manager';
423
+
424
+ const payload = { items: [1, 2, 3], total: 100 };
425
+ addObjectMetadata(span.SpanAttributes, payload, 'order.payload');
426
+
427
+ // Resultado:
428
+ // {
429
+ // "order.payload.exists": "true",
430
+ // "order.payload.type": "object",
431
+ // "order.payload.keys": "items,total",
432
+ // "order.payload.keyCount": "2"
433
+ // }
434
+ ```
435
+
436
+ ### Uso en Middleware Custom
437
+
438
+ ```ts
439
+ import {
440
+ addRequestBodyMetadata,
441
+ addResponseBodyMetadata
442
+ } from '@hemia/trace-manager';
443
+
444
+ export const customMiddleware = () => {
445
+ return (req: Request, res: Response, next: NextFunction) => {
446
+ // ... inicializar span ...
447
+
448
+ res.on('finish', () => {
449
+ const isError = res.statusCode >= 400;
450
+
451
+ if (isError) {
452
+ // Capturar metadata en lugar de cuerpos completos
453
+ addRequestBodyMetadata(
454
+ span.SpanAttributes,
455
+ req.body
456
+ );
457
+
458
+ addResponseBodyMetadata(
459
+ span.SpanAttributes,
460
+ res.locals.responseBody,
461
+ isError // Captura mensaje de error si existe
462
+ );
463
+ }
464
+ });
465
+
466
+ next();
467
+ };
468
+ };
469
+ ```
470
+
471
+ ### Ventajas del Metadata Approach
472
+
473
+ ✅ **Menor storage**: 10-20 bytes vs potencialmente KB de JSON
474
+ ✅ **Privacy by default**: No serializa valores sensibles (passwords, tokens)
475
+ ✅ **Mejor cardinality**: Más eficiente para queries en ClickHouse
476
+ ✅ **Suficiente para debugging**: Keys y tipos son suficientes para diagnosticar
477
+ ✅ **Consistencia**: Mismo approach en decoradores y middlewares
478
+
338
479
  ### `sanitizeArgs()` y `toSafeJSON()`
339
480
 
340
- Utilidades internas que previenen:
481
+ Utilidades legacy que previenen:
341
482
  - Serialización de objetos circulares
342
- - Exposición de contraseñas/tokens en atributos
483
+ - Filtrado de Request/Response de Express
343
484
  - Overflow de tamaño en ClickHouse
344
485
 
486
+ **⚠️ Nota**: El decorador `@Trace` ahora usa metadata en lugar de estas utilidades, pero siguen disponibles para compatibilidad.
487
+
345
488
  ---
346
489
 
347
490
  ## 📚 Dependencias
348
491
 
349
- - `@hemia/app-context` - Manejo de contexto con AsyncLocalStorage
350
- - `uuid` - Generación de IDs únicos para spans
492
+ - `@hemia/app-context` (^0.0.6) - Manejo de contexto con AsyncLocalStorage
493
+ - `uuid` (^10.0.0) - Generación de IDs únicos para spans
494
+
495
+ ## 🔄 Versión Actual
496
+
497
+ **v0.0.3** - Metadata approach implementado (Noviembre 2025)
351
498
 
352
499
  ---
353
500
 
@@ -1 +1 @@
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
+ 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 r={DEBUG:5,INFO:9,WARN:13,ERROR:17,FATAL:21};const n=new class{constructor(t){this.defaultServiceName=t}log(e,n,o){const s=t.getStore();if(!s)return console.warn("[Logger] No active context. Log not traced."),void console.log(`[${e}] ${n}`,o);const i={Timestamp:(new Date).toISOString(),TraceId:s.traceId,SpanId:s.spanId,TraceFlags:1,SeverityText:e,SeverityNumber:r[e],ServiceName:s.serviceName,Body:n,LogAttributes:this.stringifyAttributes(o||{}),ResourceAttributes:s.traceContext.resourceAttributes};s.traceContext.logs.push(i),"production"!==process.env.NODE_ENV&&console.log(`[${e}] [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)}};for(var o,s=[],i=0;i<256;++i)s.push((i+256).toString(16).slice(1));var a=new Uint8Array(16);function c(){if(!o&&!(o="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 o(a)}var p={randomUUID:"undefined"!=typeof crypto&&crypto.randomUUID&&crypto.randomUUID.bind(crypto)};function u(t,e,r){if(p.randomUUID&&!t)return p.randomUUID();var n=(t=t||{}).random||(t.rng||c)();return n[6]=15&n[6]|64,n[8]=63&n[8]|128,function(t,e=0){return(s[t[e+0]]+s[t[e+1]]+s[t[e+2]]+s[t[e+3]]+"-"+s[t[e+4]]+s[t[e+5]]+"-"+s[t[e+6]]+s[t[e+7]]+"-"+s[t[e+8]]+s[t[e+9]]+"-"+s[t[e+10]]+s[t[e+11]]+s[t[e+12]]+s[t[e+13]]+s[t[e+14]]+s[t[e+15]]).toLowerCase()}(n)}function g(t,e){t["app.method.args.count"]=e.length.toString(),t["app.method.args.types"]=e.map(t=>typeof t).join(",")}function y(t,e){t["app.method.result.type"]=typeof e,t["app.method.result.isArray"]=Array.isArray(e).toString(),t["app.method.result.length"]=Array.isArray(e)?e.length.toString():"1"}function d(t,e,r="http.request.body"){if(t[`${r}.exists`]=(!!e).toString(),t[`${r}.type`]=typeof e,e&&"object"==typeof e){const n=Object.keys(e);t[`${r}.keys`]=n.join(","),t[`${r}.keyCount`]=n.length.toString()}Array.isArray(e)&&(t[`${r}.isArray`]="true",t[`${r}.length`]=e.length.toString())}function l(t,e,r=!1,n="http.response.body"){if(t[`${n}.exists`]=(!!e).toString(),t[`${n}.type`]=typeof e,e&&"object"==typeof e){const r=Object.keys(e);t[`${n}.keys`]=r.join(","),t[`${n}.keyCount`]=r.length.toString()}Array.isArray(e)&&(t[`${n}.isArray`]="true",t[`${n}.length`]=e.length.toString()),r&&e&&(e.message&&(t["http.response.error.message"]=String(e.message).substring(0,500)),e.code&&(t["http.response.error.code"]=String(e.code)),e.error&&(t["http.response.error.type"]=String(e.error).substring(0,200)))}function f(t,e,r){if(e){if(t[`${r}.exists`]="true",t[`${r}.type`]=typeof e,"object"==typeof e&&!Array.isArray(e)){const n=Object.keys(e);t[`${r}.keys`]=n.join(","),t[`${r}.keyCount`]=n.length.toString()}Array.isArray(e)&&(t[`${r}.isArray`]="true",t[`${r}.length`]=e.length.toString(),e.length>0&&(t[`${r}.firstItemType`]=typeof e[0])),"string"==typeof e?t[`${r}.value`]=e.substring(0,200):"number"!=typeof e&&"boolean"!=typeof e||(t[`${r}.value`]=String(e))}else t[`${r}.exists`]="false"}function S(e){return function(r,n,o){const s=o?.value;if("function"!=typeof s)throw new Error("@Trace can only be applied to methods");o.value=async function(...o){const i=t.getStore();if(!i)return console.warn("[Trace] No active context. Skipping trace."),await s.apply(this,o);const a=u(),c=this?.constructor?.name||r.name||"UnknownClass",p=e?.name||`${c}.${n}`,d=process.hrtime.bigint(),l=(new Date).toISOString(),f=i.spanId,S={Timestamp:l,TraceId:i.traceId,SpanId:a,ParentSpanId:f,TraceState:"",ServiceName:i.serviceName,SpanName:p,SpanKind:e?.kind||"SPAN_KIND_INTERNAL",DurationUInt64:BigInt(0),StatusCode:"STATUS_CODE_UNSET",StatusMessage:"",SpanAttributes:{"code.function":n,"code.namespace":c,...e?.attributes},ResourceAttributes:i.traceContext.resourceAttributes,EventsNested:[],LinksNested:[]},m=i.spanId;i.spanId=a;try{g(S.SpanAttributes,o);const t=await s.apply(this,o);return y(S.SpanAttributes,t),S.StatusCode="STATUS_CODE_OK",t}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":String(t.stack||"").substring(0,5e3)}};throw S.EventsNested.push(e),S.StatusCode="STATUS_CODE_ERROR",S.StatusMessage=t.message||"Unhandled exception",t}finally{const t=process.hrtime.bigint();S.DurationUInt64=t-d,i.traceContext.spans.push(S),i.spanId=m}}}}export{S as Trace,e as TraceHeader,g as addArgsMetadata,f as addObjectMetadata,d as addRequestBodyMetadata,l as addResponseBodyMetadata,y as addResultMetadata,n as logger};
@@ -1 +1 @@
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
+ "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)}};for(var o,s=[],a=0;a<256;++a)s.push((a+256).toString(16).slice(1));var i=new Uint8Array(16);function c(){if(!o&&!(o="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 o(i)}var p={randomUUID:"undefined"!=typeof crypto&&crypto.randomUUID&&crypto.randomUUID.bind(crypto)};function u(t,e,r){if(p.randomUUID&&!t)return p.randomUUID();var n=(t=t||{}).random||(t.rng||c)();return n[6]=15&n[6]|64,n[8]=63&n[8]|128,function(t,e=0){return(s[t[e+0]]+s[t[e+1]]+s[t[e+2]]+s[t[e+3]]+"-"+s[t[e+4]]+s[t[e+5]]+"-"+s[t[e+6]]+s[t[e+7]]+"-"+s[t[e+8]]+s[t[e+9]]+"-"+s[t[e+10]]+s[t[e+11]]+s[t[e+12]]+s[t[e+13]]+s[t[e+14]]+s[t[e+15]]).toLowerCase()}(n)}function d(t,e){t["app.method.args.count"]=e.length.toString(),t["app.method.args.types"]=e.map(t=>typeof t).join(",")}function g(t,e){t["app.method.result.type"]=typeof e,t["app.method.result.isArray"]=Array.isArray(e).toString(),t["app.method.result.length"]=Array.isArray(e)?e.length.toString():"1"}exports.Trace=function(t){return function(r,n,o){const s=o?.value;if("function"!=typeof s)throw new Error("@Trace can only be applied to methods");o.value=async function(...o){const a=e.asyncContext.getStore();if(!a)return console.warn("[Trace] No active context. Skipping trace."),await s.apply(this,o);const i=u(),c=this?.constructor?.name||r.name||"UnknownClass",p=t?.name||`${c}.${n}`,y=process.hrtime.bigint(),l=(new Date).toISOString(),S=a.spanId,f={Timestamp:l,TraceId:a.traceId,SpanId:i,ParentSpanId:S,TraceState:"",ServiceName:a.serviceName,SpanName:p,SpanKind:t?.kind||"SPAN_KIND_INTERNAL",DurationUInt64:BigInt(0),StatusCode:"STATUS_CODE_UNSET",StatusMessage:"",SpanAttributes:{"code.function":n,"code.namespace":c,...t?.attributes},ResourceAttributes:a.traceContext.resourceAttributes,EventsNested:[],LinksNested:[]},h=a.spanId;a.spanId=i;try{d(f.SpanAttributes,o);const t=await s.apply(this,o);return g(f.SpanAttributes,t),f.StatusCode="STATUS_CODE_OK",t}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":String(t.stack||"").substring(0,5e3)}};throw f.EventsNested.push(e),f.StatusCode="STATUS_CODE_ERROR",f.StatusMessage=t.message||"Unhandled exception",t}finally{const t=process.hrtime.bigint();f.DurationUInt64=t-y,a.traceContext.spans.push(f),a.spanId=h}}}},exports.addArgsMetadata=d,exports.addObjectMetadata=function(t,e,r){if(e){if(t[`${r}.exists`]="true",t[`${r}.type`]=typeof e,"object"==typeof e&&!Array.isArray(e)){const n=Object.keys(e);t[`${r}.keys`]=n.join(","),t[`${r}.keyCount`]=n.length.toString()}Array.isArray(e)&&(t[`${r}.isArray`]="true",t[`${r}.length`]=e.length.toString(),e.length>0&&(t[`${r}.firstItemType`]=typeof e[0])),"string"==typeof e?t[`${r}.value`]=e.substring(0,200):"number"!=typeof e&&"boolean"!=typeof e||(t[`${r}.value`]=String(e))}else t[`${r}.exists`]="false"},exports.addRequestBodyMetadata=function(t,e,r="http.request.body"){if(t[`${r}.exists`]=(!!e).toString(),t[`${r}.type`]=typeof e,e&&"object"==typeof e){const n=Object.keys(e);t[`${r}.keys`]=n.join(","),t[`${r}.keyCount`]=n.length.toString()}Array.isArray(e)&&(t[`${r}.isArray`]="true",t[`${r}.length`]=e.length.toString())},exports.addResponseBodyMetadata=function(t,e,r=!1,n="http.response.body"){if(t[`${n}.exists`]=(!!e).toString(),t[`${n}.type`]=typeof e,e&&"object"==typeof e){const r=Object.keys(e);t[`${n}.keys`]=r.join(","),t[`${n}.keyCount`]=r.length.toString()}Array.isArray(e)&&(t[`${n}.isArray`]="true",t[`${n}.length`]=e.length.toString()),r&&e&&(e.message&&(t["http.response.error.message"]=String(e.message).substring(0,500)),e.code&&(t["http.response.error.code"]=String(e.code)),e.error&&(t["http.response.error.type"]=String(e.error).substring(0,200)))},exports.addResultMetadata=g,exports.logger=n;
@@ -3,3 +3,4 @@ export { TraceContext } from "./types/traceContext";
3
3
  export { TraceOptions } from "./types/traceOptions";
4
4
  export { logger } from "./log/Logger";
5
5
  export { Trace } from "./decorator/Traceable";
6
+ export { addArgsMetadata, addResultMetadata, addRequestBodyMetadata, addResponseBodyMetadata, addObjectMetadata } from "./utils/SpanAttributesHelper";
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Utilidades para agregar metadata de objetos a SpanAttributes
3
+ * siguiendo el principio de metadata sobre serialización completa
4
+ */
5
+ /**
6
+ * Agrega metadata de argumentos de entrada a los SpanAttributes
7
+ * @param attributes - Objeto de atributos donde agregar metadata
8
+ * @param args - Array de argumentos a analizar
9
+ */
10
+ export declare function addArgsMetadata(attributes: Record<string, string>, args: any[]): void;
11
+ /**
12
+ * Agrega metadata de resultado/output a los SpanAttributes
13
+ * @param attributes - Objeto de atributos donde agregar metadata
14
+ * @param result - Resultado a analizar
15
+ */
16
+ export declare function addResultMetadata(attributes: Record<string, string>, result: any): void;
17
+ /**
18
+ * Agrega metadata de Request body a SpanAttributes (para middlewares HTTP)
19
+ * @param attributes - Objeto de atributos donde agregar metadata
20
+ * @param body - Body del request
21
+ * @param prefix - Prefijo para las keys (default: 'http.request.body')
22
+ */
23
+ export declare function addRequestBodyMetadata(attributes: Record<string, string>, body: any, prefix?: string): void;
24
+ /**
25
+ * Agrega metadata de Response body a SpanAttributes (para middlewares HTTP)
26
+ * Solo agrega información extendida en caso de errores
27
+ * @param attributes - Objeto de atributos donde agregar metadata
28
+ * @param body - Body del response
29
+ * @param isError - Si es una respuesta de error (statusCode >= 400)
30
+ * @param prefix - Prefijo para las keys (default: 'http.response.body')
31
+ */
32
+ export declare function addResponseBodyMetadata(attributes: Record<string, string>, body: any, isError?: boolean, prefix?: string): void;
33
+ /**
34
+ * Agrega metadata genérica de un objeto a SpanAttributes
35
+ * Útil para analizar cualquier tipo de payload
36
+ * @param attributes - Objeto de atributos donde agregar metadata
37
+ * @param data - Objeto a analizar
38
+ * @param prefix - Prefijo para las keys
39
+ */
40
+ export declare function addObjectMetadata(attributes: Record<string, string>, data: any, prefix: string): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hemia/trace-manager",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
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,7 +13,7 @@
13
13
  "test": "jest --passWithNoTests --detectOpenHandles"
14
14
  },
15
15
  "devDependencies": {
16
- "@hemia/app-context": "^0.0.3",
16
+ "@hemia/app-context": "^0.0.6",
17
17
  "@rollup/plugin-commonjs": "^26.0.1",
18
18
  "@rollup/plugin-json": "^6.1.0",
19
19
  "@rollup/plugin-node-resolve": "^15.2.3",