@axiomhq/logging
Version:
The official logging package for Axiom
269 lines (232 loc) • 8 kB
text/typescript
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;
}