@nivinjoseph/n-log
Version:
Logging framework
313 lines (269 loc) • 10.6 kB
text/typescript
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];
}
}