UNPKG

syntropylog

Version:

An instance manager with observability for Node.js applications

197 lines 8.47 kB
/** * @file src/http/InstrumentedHttpClient.ts * @description This class is the heart of the HTTP instrumentation architecture. * It wraps any adapter that complies with `IHttpClientAdapter` and adds a centralized * layer of instrumentation (logging, context, timers). */ /** * @class InstrumentedHttpClient * @description Wraps an `IHttpClientAdapter` to provide automatic logging, * context propagation, and timing for all HTTP requests. */ export class InstrumentedHttpClient { adapter; logger; contextManager; config; instanceName; instrumentorOptions; /** * @constructor * @param {IHttpClientAdapter} adapter - The underlying HTTP client adapter (e.g., AxiosAdapter). * @param {ILogger} logger - The logger instance for this client. * @param {IContextManager} contextManager - The manager for handling asynchronous contexts. * @param {HttpClientInstanceConfig} 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; // Extract instrumentation options from the main config for clarity. this.instrumentorOptions = { logRequestHeaders: this.config.logging?.logRequestHeaders, logRequestBody: this.config.logging?.logRequestBody, logSuccessHeaders: this.config.logging?.logSuccessHeaders, logSuccessBody: this.config.logging?.logSuccessBody, logLevel: { onRequest: this.config.logging?.onRequest, onSuccess: this.config.logging?.onSuccess, onError: this.config.logging?.onError, }, }; } /** * The single public method. It executes an HTTP request through the wrapped * adapter, applying all instrumentation logic. * @template T The expected type of the response data. * @param {AdapterHttpRequest} request - The generic HTTP request to execute. * @returns {Promise<AdapterHttpResponse<T>>} A promise that resolves with the normalized response. * @throws {AdapterHttpError | Error} Throws the error from the adapter, which is re-thrown after being logged. */ async request(request) { const startTime = Date.now(); if (!request.headers) { request.headers = {}; } // 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') { request.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') { request.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') { request.headers[key] = value; } } } } // Always propagate correlation and transaction IDs, as they are fundamental. const correlationId = this.contextManager.getCorrelationId(); if (correlationId) { request.headers[this.contextManager.getCorrelationIdHeaderName()] = correlationId; } const transactionId = this.contextManager.getTransactionId(); if (transactionId) { request.headers[this.contextManager.getTransactionIdHeaderName()] = transactionId; } // 2. Log the start of the request. this.logRequestStart(request); try { // 3. Delegate execution to the adapter. const response = await this.adapter.request(request); const durationMs = Date.now() - startTime; // 4. Log the successful completion of the request. this.logRequestSuccess(request, response, durationMs); return response; } catch (error) { const durationMs = Date.now() - startTime; // 5. Log the failure of the request. this.logRequestFailure(request, error, durationMs); // 6. Re-throw the error so the user's code can handle it. throw error; } } /** * @private * Logs the start of an HTTP request, respecting the configured options. * @param {AdapterHttpRequest} request - The outgoing request. */ logRequestStart(request) { const logLevel = this.instrumentorOptions.logLevel?.onRequest ?? 'info'; const logPayload = { method: request.method, url: request.url, }; if (this.instrumentorOptions.logRequestHeaders) { logPayload.headers = request.headers; } if (this.instrumentorOptions.logRequestBody) { logPayload.body = request.body; } this.logger[logLevel](logPayload, 'Starting HTTP request'); } /** * @private * Logs the successful completion of an HTTP request. * @template T * @param {AdapterHttpRequest} request - The original request. * @param {AdapterHttpResponse<T>} response - The received response. * @param {number} durationMs - The total duration of the request in milliseconds. */ logRequestSuccess(request, response, durationMs) { const logLevel = this.instrumentorOptions.logLevel?.onSuccess ?? 'info'; const logPayload = { statusCode: response.statusCode, url: request.url, method: request.method, durationMs, }; if (this.instrumentorOptions.logSuccessHeaders) { logPayload.headers = response.headers; } if (this.instrumentorOptions.logSuccessBody) { logPayload.body = response.data; } this.logger[logLevel](logPayload, 'HTTP response received'); } /** * @private * Logs the failure of an HTTP request. * @param {AdapterHttpRequest} request - The original request. * @param {unknown} error - The error that was thrown. * @param {number} durationMs - The total duration of the request until failure. */ logRequestFailure(request, error, durationMs) { const logLevel = this.instrumentorOptions.logLevel?.onError ?? 'error'; // Use the normalized adapter error if available for richer logging. if (error && error.isAdapterError) { const adapterError = error; const logPayload = { err: adapterError, // The logger's serializer will handle this. url: request.url, method: request.method, durationMs, response: adapterError.response ? { statusCode: adapterError.response.statusCode, headers: adapterError.response.headers, body: adapterError.response.data, } : 'No response', }; this.logger[logLevel](logPayload, 'HTTP request failed'); } else { // If it's an unexpected error, log it as well. this.logger[logLevel]({ err: error, url: request.url, method: request.method, durationMs }, 'HTTP request failed with an unexpected error'); } } } //# sourceMappingURL=InstrumentedHttpClient.js.map