nightingale-logger
Version:
Logger for browser and node
700 lines (625 loc) • 16.3 kB
text/typescript
import { Level } from "nightingale-levels";
import type {
Handler,
LogRecord,
Metadata,
MetadataStyles,
Processor,
Styles,
} from "nightingale-types";
import { inspectValue } from "./inspectValue.ts";
export { Level } from "nightingale-levels";
export interface Options<T extends Metadata> {
symbol?: string;
metadataStyles?: MetadataStyles<T>;
styles?: Styles;
}
export interface ComputedConfigForKey {
handlers: Handler[];
processors: Processor[];
}
export interface ExtendedFunctionNameMetadata {
functionName: string;
}
export interface Config {
handler?: Handler;
handlers?: Handler[];
key?: string;
keys?: string[];
pattern?: RegExp;
processor?: Processor;
processors?: Processor[];
stop?: boolean;
}
declare global {
var __NIGHTINGALE_CONFIG: Config[];
var __NIGHTINGALE_LOGGER_MAP_CACHE: Map<string, ComputedConfigForKey>;
var __NIGHTINGALE_CONFIG_DEFAULT: ComputedConfigForKey;
var __NIGHTINGALE_GLOBAL_HANDLERS: unknown;
var __NIGHTINGALE_GET_CONFIG_FOR_LOGGER: (
key: string,
) => ComputedConfigForKey;
var __NIGHTINGALE_GET_CONFIG_FOR_LOGGER_RECORD: (
key: string,
level: number,
) => ComputedConfigForKey;
}
const globalOrWindow: typeof globalThis =
typeof globalThis !== "undefined" ? globalThis : globalThis;
if (!globalOrWindow.__NIGHTINGALE_GET_CONFIG_FOR_LOGGER) {
globalOrWindow.__NIGHTINGALE_GET_CONFIG_FOR_LOGGER =
(): ComputedConfigForKey => ({
handlers: [],
processors: [],
});
}
if (!globalOrWindow.__NIGHTINGALE_GET_CONFIG_FOR_LOGGER_RECORD) {
globalOrWindow.__NIGHTINGALE_GET_CONFIG_FOR_LOGGER_RECORD = (
key: string,
level: Level,
): ComputedConfigForKey => {
const { handlers, processors }: ComputedConfigForKey =
globalOrWindow.__NIGHTINGALE_GET_CONFIG_FOR_LOGGER(key);
return {
handlers: handlers.filter(
(handler) =>
level >= handler.minLevel &&
(!handler.isHandling || handler.isHandling(level, key)),
),
processors,
};
};
}
function getConfigForLoggerRecord(
key: string,
recordLevel: Level,
): ComputedConfigForKey {
return globalOrWindow.__NIGHTINGALE_GET_CONFIG_FOR_LOGGER_RECORD(
key,
recordLevel,
);
}
function isError(messageOrError: Error | string): messageOrError is Error {
return messageOrError instanceof Error;
}
/**
* Interface that allows you to log records.
* This records are treated by handlers
*/
export class Logger {
private contextObject?: Record<string, unknown>;
readonly key: string;
readonly displayName?: string;
/**
* Create a new Logger
*
* @param {string} key
* @param {string} [displayName]
*/
constructor(key: string, displayName?: string) {
this.key = key;
this.displayName = displayName;
if (process.env.NODE_ENV !== "production" && key.includes(".")) {
throw new Error(
`nightingale: \`.\` in key is no longer supported, use \`:\` instead (key: ${key})`,
);
}
}
/** @private */
protected getHandlersAndProcessors(
recordLevel: number,
): ComputedConfigForKey {
return getConfigForLoggerRecord(this.key, recordLevel);
}
/** @private */
getConfig(): Readonly<ComputedConfigForKey> {
return globalOrWindow.__NIGHTINGALE_GET_CONFIG_FOR_LOGGER(this.key);
}
/**
* Create a child logger
*/
child(childSuffixKey: string, childDisplayName?: string): Logger {
return new Logger(`${this.key}:${childSuffixKey}`, childDisplayName);
}
/**
* Create a new Logger with the same key a this attached context
*
* @example
* ```typescript
* const loggerMyService = new Logger('app:myService');
* function someAction(arg1) {
* const logger = loggerMyService.context({ arg1 });
* logger.enter(someAction);
* // do stuff
* logger.info('info');
* // do stuff
* logger.exit(someAction);
* }
* ```
*
*/
context(context: Record<string, unknown>): Logger {
const logger = new Logger(this.key);
logger.setContext(context);
return logger;
}
/**
* Get the context of this logger
*/
getContextObject(): Readonly<Record<string, unknown>> | undefined {
return this.contextObject;
}
/**
* Set the context of this logger
*
* @param {Object} context
*/
setContext(context: Record<string, unknown>): void {
this.contextObject = context;
}
/**
* Extends existing context of this logger
*/
extendsContext(extendedContext: Record<string, unknown>): void {
if (this.contextObject === undefined) {
throw new Error(
"Cannot extends context that does not exists. Use setContext(context) first.",
);
}
Object.assign(this.contextObject, extendedContext);
}
/**
* Handle a record
*
* Use this only if you know what you are doing.
*/
addRecord<T extends Metadata>(record: Readonly<LogRecord<T>>): void {
const { handlers, processors } = this.getHandlersAndProcessors(
record.level,
);
if (handlers.length === 0) {
if (record.level > Level.ERROR) {
// eslint-disable-next-line no-console
console.log("[nightingale] no logger for > error level.", {
key: record.key,
message: record.message,
});
}
return;
}
if (processors) {
processors.forEach((process) => {
process(record, record.context);
});
}
handlers.some((handler) => handler.handle(record) === false);
}
/**
* Log a message
*/
log<T extends Metadata>(
messageOrError: Error | string,
metadata?: T,
level: Level = Level.INFO,
options?: Options<T>,
): void {
const isMessageError = isError(messageOrError);
const message = isMessageError
? `${messageOrError.name}: ${messageOrError.message}`
: messageOrError;
const extendedMetadata =
isMessageError && !(metadata && Object.hasOwn(metadata, "error"))
? { ...metadata, error: messageOrError }
: metadata;
const context = extendedMetadata?.context;
if (extendedMetadata) {
delete extendedMetadata.context;
}
const record: LogRecord<NonNullable<typeof extendedMetadata>> = {
level,
key: this.key,
displayName: this.displayName,
datetime: new Date(),
message,
context: context || this.contextObject,
metadata: extendedMetadata as NonNullable<typeof extendedMetadata>,
extra: {},
...options,
};
this.addRecord(record);
}
/**
* Log a trace message
*/
trace<T extends Metadata>(
messageOrError: Error | string,
metadata?: T,
metadataStyles?: MetadataStyles<T>,
): void {
this.log(messageOrError, metadata, Level.TRACE, { metadataStyles });
}
/**
* Log a debug message
*/
debug<T extends Metadata>(
messageOrError: Error | string,
metadata?: T,
metadataStyles?: MetadataStyles<T>,
): void {
this.log(messageOrError, metadata, Level.DEBUG, { metadataStyles });
}
/**
* Notice an info message
*/
notice<T extends Metadata>(
messageOrError: Error | string,
metadata?: T,
metadataStyles?: MetadataStyles<T>,
): void {
this.log(messageOrError, metadata, Level.NOTICE, { metadataStyles });
}
/**
* Log an info message
*/
info<T extends Metadata>(
messageOrError: Error | string,
metadata?: T,
metadataStyles?: MetadataStyles<T>,
): void {
this.log(messageOrError, metadata, Level.INFO, { metadataStyles });
}
/**
* Log a warn message
*/
warn<T extends Metadata>(
messageOrError: Error | string,
metadata?: T,
metadataStyles?: MetadataStyles<T>,
): void {
this.log(messageOrError, metadata, Level.WARN, { metadataStyles });
}
/**
* Log an error message
*
* @example
* ```typescript
* const logger = new Logger('something');
* try {
* throw new Error('Always throws here');
* } catch (error) {
* logger.error('caught error', { error });
* }
* ```
*/
error<T extends Metadata>(
messageOrError: Error | string,
metadata?: T,
metadataStyles?: MetadataStyles<T>,
): void {
this.log(messageOrError, metadata, Level.ERROR, { metadataStyles });
}
/**
* Log an critical message
*/
critical<T extends Metadata>(
messageOrError: Error | string,
metadata?: T,
metadataStyles?: MetadataStyles<T>,
): void {
this.log(messageOrError, metadata, Level.CRITICAL, { metadataStyles });
}
/**
* Log a fatal message
*
* @example
* ```typescript
* const logger = new Logger('something');
* try {
* throw new Error('Always throws here');
* } catch (error) {
* logger.error('caught error', { error });
* process.exit(1);
* }
*/
fatal<T extends Metadata>(
messageOrError: Error | string,
metadata?: T,
metadataStyles?: MetadataStyles<T>,
): void {
this.log(messageOrError, metadata, Level.FATAL, { metadataStyles });
}
/**
* Log an alert message
*/
alert<T extends Metadata>(
messageOrError: Error | string,
metadata?: T,
metadataStyles?: MetadataStyles<T>,
): void {
this.log(messageOrError, metadata, Level.ALERT, { metadataStyles });
}
/**
* Log an inspected value
*/
inspectValue<T extends Metadata>(
value: unknown,
metadata?: T,
metadataStyles?: MetadataStyles<T>,
): void {
const inspectedValue = inspectValue(value);
this.log(inspectedValue, metadata, Level.DEBUG, {
metadataStyles,
styles: ["gray"],
});
}
/**
* Log a debugged var
*/
inspectVar<T extends Metadata>(
varName: string,
varValue: unknown,
metadata?: T,
metadataStyles?: MetadataStyles<T>,
): void {
const inspectedValue = inspectValue(varValue);
this.log(`${varName} = ${inspectedValue}`, metadata, Level.DEBUG, {
metadataStyles,
styles: ["cyan"],
});
}
/**
* Alias for infoSuccess
*/
success<T extends Metadata>(
message: string,
metadata?: T,
metadataStyles?: MetadataStyles<T>,
): void {
this.infoSuccess(message, metadata, metadataStyles);
}
/**
* Log an info success message
*/
infoSuccess<T extends Metadata>(
message: string,
metadata?: T,
metadataStyles?: MetadataStyles<T>,
): void {
this.log(message, metadata, Level.INFO, {
metadataStyles,
symbol: "✔",
styles: ["green", "bold"],
});
}
/**
* Log an debug success message
*/
debugSuccess<T extends Metadata>(
message: string,
metadata?: T,
metadataStyles?: MetadataStyles<T>,
): void {
this.log(message, metadata, Level.DEBUG, {
metadataStyles,
symbol: "✔",
styles: ["green"],
});
}
/**
* Alias for infoFail
*/
fail<T extends Metadata>(
messageOrError: Error | string,
metadata?: T,
metadataStyles?: MetadataStyles<T>,
): void {
this.infoFail(messageOrError, metadata, metadataStyles);
}
/**
* Log an info fail message
*/
infoFail<T extends Metadata>(
messageOrError: Error | string,
metadata?: T,
metadataStyles?: MetadataStyles<T>,
): void {
this.log(messageOrError, metadata, Level.INFO, {
metadataStyles,
symbol: "✖",
styles: ["red", "bold"],
});
}
/**
* Log an debug fail message
*/
debugFail<T extends Metadata>(
messageOrError: Error | string,
metadata?: T,
metadataStyles?: MetadataStyles<T>,
): void {
this.log(messageOrError, metadata, Level.DEBUG, {
metadataStyles,
symbol: "✖",
styles: ["red"],
});
}
/**
* @returns {number} time to pass to timeEnd
*/
time<T extends Metadata>(
message?: string,
metadata?: T,
metadataStyles?: MetadataStyles<T>,
level: number = Level.DEBUG,
): number {
if (message) {
this.log(message, metadata, level, { metadataStyles });
}
return Date.now();
}
infoTime<T extends Metadata>(
message?: string,
metadata?: T,
metadataStyles?: MetadataStyles<T>,
): number {
return this.time(message, metadata, metadataStyles, Level.INFO);
}
/**
* Finds difference between when this method
* was called and when the respective time method
* was called, then logs out the difference
* and deletes the original record
*/
timeEnd<T extends Metadata>(
startTime: number,
message: string,
metadata?: T,
metadataStyles?: MetadataStyles<T>,
level: number = Level.DEBUG,
options?: Options<T>,
): void {
const now = Date.now();
const diffTime = now - startTime;
let readableTime;
if (diffTime < 1000) {
readableTime = `${diffTime}ms`;
} else {
const seconds = diffTime > 1000 ? Math.floor(diffTime / 1000) : 0;
const ms = diffTime - seconds * 1000;
readableTime = `${seconds ? `${seconds}s and ` : ""}${ms}ms`;
}
const extendedMetadata = {
...metadata,
readableTime,
timeMs: diffTime,
};
this.log(message, extendedMetadata, level, { ...options, metadataStyles });
}
/**
* Like timeEnd, but with INFO level
*/
infoTimeEnd<T extends Metadata>(
time: number,
message: string,
metadata?: T,
metadataStyles?: MetadataStyles<T>,
): void {
this.timeEnd(time, message, metadata, metadataStyles, Level.INFO);
}
/**
* Like timeEnd, but with INFO level
*/
infoSuccessTimeEnd<T extends Metadata>(
time: number,
message: string,
metadata?: T,
metadataStyles?: MetadataStyles<T>,
): void {
this.timeEnd(time, message, metadata, metadataStyles, Level.INFO, {
symbol: "✔",
styles: ["green", "bold"],
});
}
/**
* Log an enter in a function
*
* @example
* ```typescript
* class A {
* method(arg1) {
* logger.enter(method, { arg1 });
* // Do your stuff
* }
* }
* ```
*
*/
enter<T extends Metadata, Fn extends (...args: unknown[]) => unknown>(
fn: Fn,
metadata?: T,
metadataStyles?: MetadataStyles<ExtendedFunctionNameMetadata & T>,
): void {
const extendedMetadata = {
...metadata,
functionName: fn.name,
};
this.log("enter", extendedMetadata, Level.TRACE, { metadataStyles });
}
/**
* Log an exit in a function
*
* @example
* ```typescript
* const logger = new Logger('myNamespace:A');
* class A {
* method(arg1) {
* // Do your stuff
* logger.exit(method, { arg1 });
* }
* }
* ```
*/
exit<T extends Metadata, Fn extends (...args: unknown[]) => unknown>(
fn: Fn,
metadata?: T,
metadataStyles?: MetadataStyles<ExtendedFunctionNameMetadata & T>,
): void {
const extendedMetadata = {
...metadata,
functionName: fn.name,
};
this.log("exit", extendedMetadata, Level.TRACE, { metadataStyles });
}
/**
* Wrap around a function to log enter and exit of a function
*
* @example
* ```typescript
* const logger = new Logger('myNamespace:A');
* class A {
* method() {
* logger.wrap(method, () => {
* // Do your stuff
* });
* }
* }
* ```
*/
wrap<Fn extends (...args: unknown[]) => unknown>(
fn: Fn,
callback: () => void,
): void;
wrap<T extends Metadata, Fn extends (...args: unknown[]) => unknown>(
fn: Fn,
metadata: T,
callback: () => void,
): void;
wrap<T extends Metadata, Fn extends (...args: unknown[]) => unknown>(
fn: Fn,
metadata: T,
metadataStyles: MetadataStyles<T>,
callback: () => void,
): void;
wrap<T extends Metadata, Fn extends (...args: unknown[]) => unknown>(
fn: Fn,
option1: T | (() => void),
option2?: MetadataStyles<T> | (() => void),
callback?: () => void,
): void {
let metadata: T | undefined;
let metadataStyles: MetadataStyles<T> | undefined;
if (typeof option1 === "function") {
callback = option1;
} else {
metadata = option1;
if (typeof option2 === "function") {
callback = option2;
} else {
metadataStyles = option2;
}
}
this.enter(fn, metadata, metadataStyles);
(callback as () => void)();
this.exit(fn);
}
}