UNPKG

@axiomhq/logging

Version:
269 lines (232 loc) 8 kB
import { defaultFormatters } from 'src/default-formatters'; import { Transport } from '.'; import { Version, isBrowser } from './runtime'; const LOG_LEVEL = 'info'; /** * Symbol used to specify properties that should be added to the root of the log event * rather than to the fields property. * * @example * const EVENT = Symbol.for('logging.event'); * logger.info("User logged in", { * userId: 123, * [EVENT]: { traceId: "abc123" } * }); */ export const EVENT = Symbol.for('logging.event'); /** * LogEvent interface representing a log entry. * This interface defines the structure of log events processed by the logger. */ export interface LogEvent extends Record<string, any> { level: string; message: string; fields: any; _time: string; '@app': { [key: FrameworkIdentifier['name']]: FrameworkIdentifier['version']; }; source: string; } export const LogLevelValue = { debug: 0, info: 1, warn: 2, error: 3, off: 100, } as const; export const LogLevel = { debug: 'debug', info: 'info', warn: 'warn', error: 'error', off: 'off', } as const; export type LogLevelValue = (typeof LogLevelValue)[keyof typeof LogLevelValue]; export type LogLevel = keyof typeof LogLevelValue; export type Formatter<T extends Record<string, any> = LogEvent, U extends Record<string, any> = LogEvent> = ( logEvent: T, ) => U; export type FrameworkIdentifier = { name: `${string}-version`; version: string; }; export type LoggerConfig = { args?: Record<string | symbol, any>; transports: [Transport, ...Transport[]]; logLevel?: LogLevel; formatters?: Array<Formatter>; overrideDefaultFormatters?: boolean; }; export class Logger { children: Logger[] = []; public logLevel: LogLevelValue = LogLevelValue.debug; public config: LoggerConfig; constructor(public initConfig: LoggerConfig) { // check if user passed a log level, if not the default init value will be used as is. if (this.initConfig.logLevel != undefined) { this.logLevel = LogLevelValue[this.initConfig.logLevel]; } else if (LOG_LEVEL) { this.logLevel = LogLevelValue[LOG_LEVEL as LogLevel]; } this.config = { ...initConfig }; if (!this.config.overrideDefaultFormatters) { this.config.formatters = [...defaultFormatters, ...(this.config.formatters ?? [])]; } } raw(log: any) { this.config.transports.forEach((transport) => transport.log([log])); } /** * Log a debug message * @param message The log message * @param options Log options that can include fields and a special EVENT symbol * * @example * // Add fields to the log event * logger.debug("User action", { userId: 123 }); */ debug = (message: string, args: Record<string | symbol, any> = {}) => { this.log(LogLevel.debug, message, args); }; /** * Log an info message * @param message The log message * @param options Log options that can include fields and a special EVENT symbol * * @example * // Add fields to the log event * logger.info("User logged in", { userId: 123 }); */ info = (message: string, args: Record<string | symbol, any> = {}) => { this.log(LogLevel.info, message, args); }; /** * Log a warning message * @param message The log message * @param options Log options that can include fields and a special EVENT symbol * * @example * // Add fields to the log event * logger.warn("Rate limit approaching", { requestCount: 950 }); */ warn = (message: string, args: Record<string | symbol, any> = {}) => { this.log(LogLevel.warn, message, args); }; /** * Log an error message * @param message The log message * @param options Log options that can include fields and a special EVENT symbol * * @example * // Log an error with stack trace * try { * // some code that throws * } catch (err) { * logger.error("Operation failed", err); * } */ error = (message: string, args: Record<string | symbol, any> = {}) => { this.log(LogLevel.error, message, args); }; /** * Create a child logger with additional context fields * @param fields Additional context fields to include in all logs from this logger * * @example * // Create a child logger with additional fields * const childLogger = logger.with({ userId: 123 }); */ with = (fields: Record<string | symbol, any>) => { const { [EVENT]: argsEventFields, ...argsRest } = this.config.args ?? {}; const { [EVENT]: _eventFields, ...rest } = fields; const eventFields = { ...(argsEventFields ?? {}), ...(_eventFields ?? {}) }; const childConfig = { ...this.config, args: { ...argsRest, ...rest, [EVENT]: eventFields } }; const child = new Logger(childConfig); this.children.push(child); return child; }; private _transformEvent = (level: LogLevel, message: string, args: Record<string | symbol, any> = {}) => { let rootFields = {}; let fields = this.config.args ?? {}; if (this.config.args && EVENT in this.config.args) { const { [EVENT]: argsEventFields, ...argsRest } = this.config.args ?? {}; rootFields = { ...(argsEventFields ?? {}) }; fields = argsRest; } const logEvent: LogEvent = { level: LogLevel[level].toString(), message, _time: new Date(Date.now()).toISOString(), fields: fields, '@app': { 'axiom-logging-version': Version ?? 'unknown', }, source: isBrowser ? 'browser-log' : 'server-log', }; // Apply root properties from logger config if present if (rootFields && typeof rootFields === 'object') { Object.assign(logEvent, rootFields); } // Handle Error objects if (args instanceof Error) { logEvent.fields = { ...logEvent.fields, message: args.message, stack: args.stack, name: args.name, }; } if (typeof args === 'object' && args !== null) { // Extract root properties before JSON serialization (since symbols are lost in JSON.stringify) const { [EVENT]: rootArgs, ...fieldArgs } = args as Record<string | symbol, any>; // Process regular fields const parsedArgs = JSON.parse(JSON.stringify(fieldArgs, jsonFriendlyErrorReplacer)); // Apply root properties directly to the root of logEvent if (rootArgs && typeof rootArgs === 'object' && rootArgs !== null) { Object.assign(logEvent, rootArgs); } // Any remaining properties in options are treated as fields if (Object.keys(parsedArgs).length > 0) { logEvent.fields = { ...logEvent.fields, ...parsedArgs }; } // Handle array-like values } else if (Array.isArray(args)) { logEvent.fields = { ...logEvent.fields, args: args }; } if (this.config.formatters && this.config.formatters.length > 0) { // Apply formatters to the entire logEvent return this.config.formatters.reduce((acc, formatter) => formatter(acc), logEvent); } return logEvent; }; /** * Log a message with the specified level * @param level The log level * @param message The log message * @param options Log options or Error object */ log = (level: LogLevel, message: string, args: Record<string | symbol, any> = {}) => { this.config.transports.forEach((transport) => transport.log([this._transformEvent(level, message, args)])); }; flush = async () => { const promises = [ ...this.config.transports.map((transport) => transport.flush()), ...this.children.map((child) => child.flush()), ]; await Promise.allSettled(promises); }; } function jsonFriendlyErrorReplacer(_key: string, value: any) { if (value instanceof Error) { return { // Pull all enumerable properties, supporting properties on custom Errors ...value, // Explicitly pull Error's non-enumerable properties name: value.name, message: value.message, stack: value.stack, }; } return value; }