import globals from "./globals"; import { LogHook } from "./hooks"; import LogLevel, { logLevelToString } from "./levels"; /* eslint-disable @typescript-eslint/unified-signatures */ /** * A simple logger that can be used to log structured data */ export default class Logger { /** * The function to call to output a JSON line log message */ private readonly out: (obj: unknown) => void; /** * Base fields that will be included in every log message sent by this logger */ private readonly baseFields: FieldsObject; /** * The level this logger is configured to log at */ private readonly logLevel: LogLevel = LogLevel.Info; /** * Any hooks that have been added to this logger */ private readonly hooks: LogHook[]; /** * Creates a new logger * @param out The function to call to output the JSON based log message * @param baseFields Any fields to include in every log message * @param hooks Any hooks to apply to every log message */ constructor( out: (obj: unknown) => void, baseFields?: FieldsObject, hooks?: LogHook[], ) { this.out = out; this.baseFields = baseFields ?? {}; this.hooks = hooks ?? []; } /** * Trace logs a message at the trace level */ trace(msg: string, fields?: FieldsObject) { this.log(LogLevel.Trace, msg, fields); } /** * Debug logs a message at the debug level */ debug(msg: string, fields?: FieldsObject) { this.log(LogLevel.Debug, msg, fields); } /** * Info logs a message at the info level */ info(msg: string, fields?: FieldsObject) { this.log(LogLevel.Info, msg, fields); } /** * Warn logs a message at the warn level */ warn(err: Error | unknown, fields?: FieldsObject): void; warn(err: Error | unknown, msg: string, fields?: FieldsObject): void; warn(msg: string, fields?: FieldsObject): void; warn(errOrMsg: unknown, msgOrFields: unknown, fields?: unknown) { this.log(LogLevel.Warn, errOrMsg, msgOrFields, fields); } /** * Error logs a message at the error level */ error(err: Error | unknown, fields?: FieldsObject): void; error(err: Error | unknown, msg: string, fields?: FieldsObject): void; error(msg: string, fields?: FieldsObject): void; error(errOrMsg: unknown, msgOrFields: unknown, fields?: unknown) { this.log(LogLevel.Error, errOrMsg, msgOrFields, fields); } /** * Fatal logs a message at the fatal level */ fatal(err: Error, fields?: FieldsObject): void; fatal(err: Error, msg: string, fields?: FieldsObject): void; fatal(msg: string, fields?: FieldsObject): void; fatal(errOrMsg: unknown, msgOrFields: unknown, fields?: unknown) { this.log(LogLevel.Fatal, errOrMsg, msgOrFields, fields); } /** * with returns a new logger with the given fields added to all log messages * created by it */ with(fields: FieldsObject): Logger { return new Logger(this.out, { ...this.baseFields, ...fields }); } /** * hook returns a new logger with the given hooks applied to all log messages * on top of any existing hooks this logger has */ hook(...newHooks: LogHook[]): Logger { if (newHooks.length === 0) { return this; } return new Logger(this.out, this.baseFields, [...this.hooks, ...newHooks]); } /** * the actual logging implementation */ private log( level: LogLevel, errOrMsg: unknown, msgOrFields?: unknown, possibleFields?: unknown, ) { if (this.logLevel <= level) { let err: Error | unknown | undefined; let msg: string; let fields: FieldsObject | undefined; // Parse the arguments if (typeof errOrMsg === "string") { // log(msg, fields?) err = undefined; msg = errOrMsg; fields = msgOrFields as FieldsObject | undefined; if (possibleFields) { throw new Error("Invalid arguments to log"); } } else if (typeof msgOrFields === "string") { // log(err, msg, fields?) err = errOrMsg; msg = msgOrFields; fields = possibleFields as FieldsObject | undefined; } else { // log(err, fields?) err = errOrMsg; msg = ""; fields = msgOrFields as FieldsObject | undefined; if (possibleFields) { throw new Error("Invalid arguments to log"); } } // Create the base object const obj: FieldsObject = { ...this.baseFields, ...(fields ?? {}), ...globals.ParamHook(), [globals.TimestampFieldName]: globals.TimestampFieldFormatter( new Date(), ), [globals.LevelFieldName]: logLevelToString(level), [globals.MessageFieldName]: msg, }; // Add the error if present if (err) { obj[globals.ErrorFieldName] = globals.ErrorFieldFormatter(err); obj[globals.StackFieldName] = globals.StackFieldFormatter(err); } // Apply the hooks for (const hook of this.hooks) { hook(obj, level, msg); } this.out(obj); } } } /** * A field value we support logging */ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error export type FieldValue = | string | number | boolean | null | undefined | FieldsObject | FieldValue[]; /** * A map of fields that can be logged */ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error export type FieldsObject = Record;