UNPKG

@nivinjoseph/n-log

Version:
313 lines (269 loc) 10.6 kB
import { ConfigurationManager } from "@nivinjoseph/n-config"; import { Exception } from "@nivinjoseph/n-exception"; import { SpanStatusCode, context, isSpanContextValid, trace } from "@opentelemetry/api"; import { LogDateTimeZone } from "./log-date-time-zone.js"; import { LogRecord } from "./log-record.js"; import { Logger } from "./logger.js"; import { LoggerConfig } from "./logger-config.js"; import { DateTime } from "luxon"; import { ensureExhaustiveCheck } from "@nivinjoseph/n-defensive"; /** * Abstract base class that provides common logging functionality. * Implements the Logger interface and provides shared functionality for all logger implementations. * Handles common tasks like timestamp formatting, error message extraction, and trace injection. */ export abstract class BaseLogger implements Logger { // eslint-disable-next-line @typescript-eslint/naming-convention private readonly _UINT_MAX = 4294967296; private readonly _source = "nodejs"; private readonly _service = ConfigurationManager.getConfig<string | null>("package_name") ?? ConfigurationManager.getConfig<string | null>("package.name") ?? "n-log"; private readonly _env = ConfigurationManager.getConfig<string | null>("env")?.toLowerCase() ?? "dev"; private readonly _logDateTimeZone: LogDateTimeZone; private readonly _useJsonFormat: boolean; private readonly _logInjector: ((record: LogRecord) => LogRecord) | null; private readonly _enableOtelToDatadogTraceConversion: boolean; /** * Gets the source identifier for logs (default: "nodejs") */ protected get source(): string { return this._source; } /** * Gets the service name for logs (default: package name or "n-log") */ protected get service(): string { return this._service; } /** * Gets the environment identifier for logs (default: "dev") */ protected get env(): string { return this._env; } /** * Gets whether JSON format is enabled for logs */ protected get useJsonFormat(): boolean { return this._useJsonFormat; } /** * Gets the log record injector function if configured */ protected get logInjector(): ((record: LogRecord) => LogRecord) | null { return this._logInjector; } /** * Creates a new instance of BaseLogger * @param config - Optional configuration for the logger * @param config.logDateTimeZone - The timezone to use for log timestamps (default: UTC) * @param config.useJsonFormat - Whether to format logs as JSON (default: false) * @param config.logInjector - Function to inject additional data into log records (only used when useJsonFormat is true) * @param config.enableOtelToDatadogTraceConversion - Whether to enable OpenTelemetry to Datadog trace ID conversion */ public constructor(config?: LoggerConfig) { // eslint-disable-next-line @typescript-eslint/unbound-method const { logDateTimeZone, useJsonFormat, logInjector, enableOtelToDatadogTraceConversion } = config ?? {}; if (!logDateTimeZone || logDateTimeZone.isEmptyOrWhiteSpace() || ![LogDateTimeZone.utc, LogDateTimeZone.local, LogDateTimeZone.est, LogDateTimeZone.pst].contains(logDateTimeZone)) { this._logDateTimeZone = LogDateTimeZone.utc; } else { this._logDateTimeZone = logDateTimeZone; } this._useJsonFormat = !!useJsonFormat; this._logInjector = logInjector ?? null; this._enableOtelToDatadogTraceConversion = !!enableOtelToDatadogTraceConversion; } /** * Logs a debug message * @param debug - The debug message to log */ public abstract logDebug(debug: string): Promise<void>; /** * Logs an informational message * @param info - The informational message to log */ public abstract logInfo(info: string): Promise<void>; /** * Logs a warning message or exception * @param warning - The warning message or exception to log */ public abstract logWarning(warning: string | Exception): Promise<void>; /** * Logs an error message or exception * @param error - The error message or exception to log */ public abstract logError(error: string | Exception): Promise<void>; /** * Extracts an error message from an exception or error object * @param exp - The exception or error to extract the message from * @returns The extracted error message */ protected getErrorMessage(exp: Exception | Error | any): string { // eslint-disable-next-line no-useless-assignment let logMessage = ""; try { if (exp instanceof Exception) logMessage = exp.toString(); else if (exp instanceof Error) logMessage = exp.stack!; else logMessage = (<object>exp).toString(); } catch (error) { console.warn(error); logMessage = "There was an error while attempting to log another error. Check earlier logs for a warning."; } return logMessage; } /** * Gets the current date and time in the configured timezone * @returns ISO formatted date-time string */ protected getDateTime(): { dateTime: string; time: string; } { const value = DateTime.now(); const time = value.toUTC().toISO(); let dateTime: string; switch (this._logDateTimeZone) { case LogDateTimeZone.utc: dateTime = time; break; case LogDateTimeZone.local: dateTime = value.toISO()!; break; case LogDateTimeZone.est: dateTime = value.setZone("America/New_York").toISO()!; break; case LogDateTimeZone.pst: dateTime = value.setZone("America/Los_Angeles").toISO()!; break; default: ensureExhaustiveCheck(this._logDateTimeZone); } return { dateTime, time }; } /** * Injects trace information into a log record * @param log - The log record to inject trace information into * @param isError - Whether this is an error log (affects span status) */ protected injectTrace(log: LogRecord & Record<string, any>, isError = false): void { const span = trace.getSpan(context.active()); if (span) { const spanContext = span.spanContext(); if (isSpanContextValid(spanContext)) { log["trace_id"] = spanContext.traceId; log["span_id"] = spanContext.spanId; log["trace_flags"] = `0${spanContext.traceFlags.toString(16)}`; if (isError) span.setStatus({ code: SpanStatusCode.ERROR, message: log.message }); if (this._enableOtelToDatadogTraceConversion) { const traceIdEnd = spanContext.traceId.slice(spanContext.traceId.length / 2); log["dd.trace_id"] = this._toNumberString(this._fromString(traceIdEnd, 16)); log["dd.span_id"] = this._toNumberString(this._fromString(spanContext.spanId, 16)); } } } } /** * Converts a buffer to a number string with the specified radix * @param buffer - The buffer to convert * @param radix - The radix to use for conversion (default: 10) * @returns The converted number string */ private _toNumberString(buffer: Uint8Array, radix?: number): string { let high = this._readInt32(buffer, 0); let low = this._readInt32(buffer, 4); let str = ""; radix = radix ?? 10; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, no-constant-condition while (1) { const mod = (high % radix) * this._UINT_MAX + low; high = Math.floor(high / radix); low = Math.floor(mod / radix); str = (mod % radix).toString(radix) + str; if (!high && !low) break; } return str; } /** * Converts a numerical string to a buffer using the specified radix * @param str - The string to convert * @param radix - The radix to use for conversion * @returns The converted buffer */ private _fromString(str: string, radix: number): Uint8Array { const buffer = new Uint8Array(8); const len = str.length; let pos = 0; let high = 0; let low = 0; // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with if (str[0] === "-") pos++; const sign = pos; while (pos < len) { const chr = parseInt(str[pos++], radix); if (!(chr >= 0)) break; // NaN low = low * radix + chr; high = high * radix + Math.floor(low / this._UINT_MAX); low %= this._UINT_MAX; } if (sign) { high = ~high; if (low) { low = this._UINT_MAX - low; } else { high++; } } this._writeUInt32BE(buffer, high, 0); this._writeUInt32BE(buffer, low, 4); return buffer; } /** * Writes an unsigned 32-bit integer to a buffer in big-endian format * @param buffer - The buffer to write to * @param value - The value to write * @param offset - The offset in the buffer to write at */ private _writeUInt32BE(buffer: Uint8Array, value: number, offset: number): void { buffer[3 + offset] = value & 255; value = value >> 8; buffer[2 + offset] = value & 255; value = value >> 8; buffer[1 + offset] = value & 255; value = value >> 8; buffer[0 + offset] = value & 255; } /** * Reads a 32-bit integer from a buffer * @param buffer - The buffer to read from * @param offset - The offset in the buffer to read from * @returns The read integer value */ private _readInt32(buffer: Uint8Array, offset: number): number { return (buffer[offset + 0] * 16777216) + (buffer[offset + 1] << 16) + (buffer[offset + 2] << 8) + buffer[offset + 3]; } }