syntropylog
Version:
An instance manager with observability for Node.js applications
233 lines • 9.4 kB
JavaScript
// @file src/context/ContextManager.ts
// @description The default implementation of the IContextManager interface. It uses Node.js's
// `AsyncLocalStorage` to create and manage asynchronous contexts, enabling
// seamless propagation of data like correlation IDs across async operations.
import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'crypto';
/**
* Manages asynchronous context using Node.js `AsyncLocalStorage`.
* This is the core component for propagating context-specific data
* (like correlation IDs) without passing them through function arguments.
* @implements {IContextManager}
*/
export class ContextManager {
storage = new AsyncLocalStorage();
correlationIdHeader = 'x-correlation-id';
transactionIdHeader = 'x-trace-id';
loggingMatrix;
constructor(loggingMatrix) {
this.storage = new AsyncLocalStorage();
this.loggingMatrix = loggingMatrix;
}
configure(options) {
if (options.correlationIdHeader) {
this.correlationIdHeader = options.correlationIdHeader;
}
if (options.transactionIdHeader) {
this.transactionIdHeader = options.transactionIdHeader;
}
}
/**
* Reconfigures the logging matrix dynamically.
* This method allows changing which context fields are included in logs
* without affecting security configurations like masking or log levels.
* @param newMatrix The new logging matrix configuration
*/
reconfigureLoggingMatrix(newMatrix) {
this.loggingMatrix = newMatrix;
}
/**
* Executes a function within a new, isolated asynchronous context.
* Any data set via `set()` inside the callback will only be available
* within that callback's asynchronous execution path. The new context
* inherits values from the parent context, if one exists.
* @template T The return type of the callback.
* @param callback The function to execute within the new context.
* @returns {T} The result of the callback function.
*/
run(fn) {
return new Promise((resolve, reject) => {
const parentContext = this.storage.getStore();
const newContextData = new Map(parentContext?.data);
this.storage.run({ data: newContextData }, async () => {
try {
await Promise.resolve(fn());
resolve();
}
catch (error) {
reject(error);
}
});
});
}
/**
* Gets a value from the current asynchronous context by its key.
* @template T The expected type of the value.
* @param key The key of the value to retrieve.
* @returns The value, or `undefined` if not found or if outside a context.
*/
get(key) {
return this.storage.getStore()?.data.get(key);
}
/**
* Gets the entire key-value store from the current asynchronous context.
* @returns {ContextData} An object containing all context data, or an empty object if outside a context.
*/
getAll() {
const store = this.storage.getStore();
if (!store) {
return {};
}
return Object.fromEntries(store.data.entries());
}
/**
* Sets a key-value pair in the current asynchronous context. This will have
* no effect if called outside of a context created by `run()`.
* This will only work if called within a context created by `run()`.
* @param key The key for the value.
* @param value The value to store.
* @returns {void}
*/
set(key, value) {
const store = this.storage.getStore();
if (store) {
store.data.set(key, value);
}
}
/**
* Gets the correlation ID from the current context.
* If no correlation ID exists, generates one automatically to ensure tracing continuity.
* @returns {string} The correlation ID (never undefined).
*/
getCorrelationId() {
let correlationId = this.get(this.correlationIdHeader) || this.get('correlationId');
if (!correlationId || typeof correlationId !== 'string') {
// Generate correlationId if none exists to ensure tracing continuity
correlationId = randomUUID();
this.set(this.correlationIdHeader, correlationId);
}
return correlationId;
}
/**
* Sets the correlation ID in the current context.
* This sets the value in the configured header name.
* @param correlationId The correlation ID to set.
*/
setCorrelationId(correlationId) {
this.set(this.correlationIdHeader, correlationId);
}
/**
* Gets the transaction ID from the current context.
* @returns {string | undefined} The transaction ID, or undefined if not set.
*/
getTransactionId() {
return this.get('transactionId');
}
/**
* Sets the transaction ID in the current context.
* @param transactionId The transaction ID to set.
*/
setTransactionId(transactionId) {
this.set('transactionId', transactionId);
}
/**
* Gets the configured HTTP header name for the correlation ID.
* @returns {string} The header name.
*/
getCorrelationIdHeaderName() {
return this.correlationIdHeader;
}
getTransactionIdHeaderName() {
return this.transactionIdHeader;
}
/**
* Gets the tracing headers to propagate the context (e.g., W3C Trace Context).
* This base implementation does not support trace context propagation.
* @returns `undefined` as this feature is not implemented by default.
*/
getTraceContextHeaders() {
const headers = {};
// Only include headers if we're inside an active context
const store = this.storage.getStore();
if (!store) {
return headers; // Return empty object if outside context
}
const correlationId = this.getCorrelationId();
const transactionId = this.getTransactionId();
if (correlationId) {
headers[this.getCorrelationIdHeaderName()] = correlationId;
}
if (transactionId) {
headers[this.getTransactionIdHeaderName()] = transactionId;
}
return headers;
}
getFilteredContext(level) {
const fullContext = this.getAll();
if (!this.loggingMatrix) {
// Si no hay loggingMatrix, siempre incluir el correlationId
const context = { ...fullContext };
const headerCorrelationId = this.get(this.correlationIdHeader);
const internalCorrelationId = this.get('correlationId');
// Si no existe el correlationId del header, usar el interno
if (!headerCorrelationId && internalCorrelationId) {
context[this.correlationIdHeader] = internalCorrelationId;
}
return context;
}
const fieldsToKeep = this.loggingMatrix[level] ?? this.loggingMatrix.default;
if (!fieldsToKeep) {
return {};
}
// Mapeo de nombres de campos del loggingMatrix a claves reales del contexto
const fieldMapping = {
correlationId: [this.correlationIdHeader, 'correlationId'],
transactionId: [this.transactionIdHeader, 'transactionId'],
userId: ['userId'],
serviceName: ['serviceName'],
operation: ['operation'],
errorCode: ['errorCode'],
tenantId: ['tenantId'],
paymentId: ['paymentId'],
orderId: ['orderId'],
processorId: ['processorId'],
eventType: ['eventType'],
};
if (fieldsToKeep.includes('*')) {
// Apply field mapping even for wildcard to ensure consistency
const mappedContext = {};
// Map all fields using the same logic as specific fields
for (const [key, value] of Object.entries(fullContext)) {
// Find the mapped field name for this key
let mappedFieldName = key;
for (const [matrixField, possibleKeys] of Object.entries(fieldMapping)) {
if (possibleKeys.includes(key)) {
mappedFieldName = matrixField;
break;
}
}
mappedContext[mappedFieldName] = value;
}
return mappedContext;
}
const filteredContext = {};
for (const field of fieldsToKeep) {
// Buscar en el mapeo de campos
const possibleKeys = fieldMapping[field] || [field];
// Buscar la primera clave que exista en el contexto
for (const key of possibleKeys) {
if (Object.prototype.hasOwnProperty.call(fullContext, key)) {
filteredContext[field] = fullContext[key];
break;
}
}
// Si no se encontrĂ³ en el mapeo, buscar directamente
if (!Object.prototype.hasOwnProperty.call(filteredContext, field) &&
Object.prototype.hasOwnProperty.call(fullContext, field)) {
filteredContext[field] = fullContext[field];
}
}
return filteredContext;
}
}
//# sourceMappingURL=ContextManager.js.map