UNPKG

syntropylog

Version:

An instance manager with observability for Node.js applications

194 lines 9.6 kB
/** * @class InstrumentedBrokerClient * @description Wraps a user-provided broker adapter to automatically handle * logging, context propagation, and distributed tracing. */ export class InstrumentedBrokerClient { adapter; logger; contextManager; config; instanceName; /** * @constructor * @param {IBrokerAdapter} adapter - The concrete broker adapter implementation (e.g., for RabbitMQ, Kafka). * @param {ILogger} logger - The logger instance for this client. * @param {IContextManager} contextManager - The manager for handling asynchronous contexts. * @param {BrokerInstanceConfig} config - The configuration for this specific instance. */ constructor(adapter, logger, contextManager, config) { this.adapter = adapter; this.logger = logger; this.contextManager = contextManager; this.config = config; this.instanceName = config.instanceName; } /** * Establishes a connection to the broker, wrapping the adapter's connect * method with logging. * @returns {Promise<void>} */ async connect() { this.logger.info('Connecting to broker...'); await this.adapter.connect(); this.logger.info('Successfully connected to broker.'); } /** * Disconnects from the broker, wrapping the adapter's disconnect method * with logging. * @returns {Promise<void>} */ async disconnect() { this.logger.info('Disconnecting from broker...'); await this.adapter.disconnect(); this.logger.info('Successfully disconnected from broker.'); } /** * Publishes a message, automatically injecting the current `correlation-id` * from the active context into the message headers. * @param {string} topic - The destination topic or routing key for the message. * @param {BrokerMessage} message - The message to be published. The `correlation-id` * will be added to its headers if not present. * @returns {Promise<void>} */ async publish(topic, message) { if (!message.headers) { message.headers = {}; } // Get current correlation ID from the active context (only if it exists, don't generate new) const currentCorrelationId = (this.contextManager.get(this.contextManager.getCorrelationIdHeaderName()) || this.contextManager.get('correlationId')); // 1. Inject context into headers based on the configuration. if (this.config.propagate?.includes('*')) { // Wildcard behavior: Propagate the entire context map. const contextObject = this.contextManager.getAll(); for (const key in contextObject) { if (Object.prototype.hasOwnProperty.call(contextObject, key)) { const value = contextObject[key]; if (typeof value === 'string' || Buffer.isBuffer(value)) { message.headers[key] = value; } } } } else if (this.config.propagate && Array.isArray(this.config.propagate)) { // New behavior: Propagate only specified context keys. for (const key of this.config.propagate) { const value = this.contextManager.get(key); if (typeof value === 'string' || Buffer.isBuffer(value)) { message.headers[key] = value; } } } else if (this.config.propagateFullContext) { // DEPRECATED: Propagate the entire context map. const contextObject = this.contextManager.getAll(); for (const key in contextObject) { if (Object.prototype.hasOwnProperty.call(contextObject, key)) { const value = contextObject[key]; if (typeof value === 'string' || Buffer.isBuffer(value)) { // Note: Broker headers typically support string | Buffer. message.headers[key] = value; } } } } // Only propagate correlation ID if it exists in the context (don't generate new) if (currentCorrelationId) { message.headers[this.contextManager.getCorrelationIdHeaderName()] = currentCorrelationId; } const transactionId = this.contextManager.getTransactionId(); if (transactionId) { message.headers[this.contextManager.getTransactionIdHeaderName()] = transactionId; } this.logger.info({ topic, messageId: message.headers?.['id'] instanceof Buffer ? message.headers?.['id'].toString() : message.headers?.['id'], correlationId: currentCorrelationId, // Log the correlation ID being used }, 'Publishing message...'); await this.adapter.publish(topic, message); this.logger.info({ topic, messageId: message.headers?.['id'] instanceof Buffer ? message.headers?.['id'].toString() : message.headers?.['id'], correlationId: currentCorrelationId, // Log the correlation ID being used }, 'Message published successfully.'); } /** * Subscribes to a topic. It wraps the user's message handler to automatically * create a new asynchronous context for each incoming message. If a `correlation-id` * is found in the message headers, it is used to initialize the new context. * @param {string} topic - The topic or queue to subscribe to. * @param {MessageHandler} handler - The user-provided function to process incoming messages. * @returns {Promise<void>} */ async subscribe(topic, handler) { this.logger.info({ topic }, 'Subscribing to topic...'); // Wrap the user's handler to implement automatic context propagation. const instrumentedHandler = async (message, controls) => { // Get correlation ID from message headers first const messageCorrelationId = message.headers?.[this.contextManager.getCorrelationIdHeaderName()]; // Get current correlation ID from context (but don't generate new one if not exists) const currentCorrelationId = this.contextManager.get(this.contextManager.getCorrelationIdHeaderName()); // If message has different correlation ID, restore context from message if (messageCorrelationId && messageCorrelationId !== currentCorrelationId) { await this.contextManager.run(async () => { if (message.headers) { for (const key in message.headers) { this.contextManager.set(key, message.headers[key]); } } // Use the message correlation ID for logging instead of generating a new one const correlationId = messageCorrelationId; this.logger.info({ topic, correlationId }, 'Received message.'); // Also wrap the lifecycle controls to add logging for ack/nack actions. const instrumentedControls = { ack: async () => { await controls.ack(); this.logger.debug({ topic, correlationId }, 'Message acknowledged (ack).'); }, nack: async (requeue) => { await controls.nack(requeue); this.logger.warn({ topic, correlationId, requeue }, 'Message negatively acknowledged (nack).'); }, }; // Execute the original user-provided handler. await handler(message, instrumentedControls); }); } else { // Use current context, just set message headers if needed if (message.headers) { for (const key in message.headers) { this.contextManager.set(key, message.headers[key]); } } // Use the message correlation ID if available, otherwise use current context (but don't generate new one) const correlationId = messageCorrelationId || this.contextManager.get(this.contextManager.getCorrelationIdHeaderName()); this.logger.info({ topic, correlationId }, 'Received message.'); // Also wrap the lifecycle controls to add logging for ack/nack actions. const instrumentedControls = { ack: async () => { await controls.ack(); this.logger.debug({ topic, correlationId }, 'Message acknowledged (ack).'); }, nack: async (requeue) => { await controls.nack(requeue); this.logger.warn({ topic, correlationId, requeue }, 'Message negatively acknowledged (nack).'); }, }; // Execute the original user-provided handler. await handler(message, instrumentedControls); } }; await this.adapter.subscribe(topic, instrumentedHandler); this.logger.info({ topic }, 'Successfully subscribed to topic.'); } } //# sourceMappingURL=InstrumentedBrokerClient.js.map