import { Time, Task, runTask, Result } from '@koober/std'; import { randomUUID as randomUUIDImplementation, UUID } from '@koober/uuid'; import { LogRecord } from './record'; import { LogDispatcher } from './dispatcher'; import { LogMessage } from './message'; import { LogLevel } from './level'; export interface Logger extends Readonly<{ dispatcher: LogDispatcher.Module; loggerMiddlewares: ReadonlyArray; loggerName: string; currentTime: Task.Sync; randomUUID: Task.Sync; }> {} export namespace Logger { export type Middleware = (record: LogRecord) => LogRecord; export type Params = Pick; export const defaultCurrentTime: Logger['currentTime'] = Time.now; export const defaultRandomUUID: Logger['randomUUID'] = randomUUIDImplementation; /** * Construct LoggerType * * @param data - constructor parameters * @returns a new LoggerType */ export function create({ loggerName, loggerMiddlewares, dispatcher, currentTime = defaultCurrentTime, randomUUID = defaultRandomUUID, }: { dispatcher: Logger['dispatcher']; loggerMiddlewares: Logger['loggerMiddlewares']; loggerName: Logger['loggerName']; currentTime?: Logger['currentTime']; randomUUID?: Logger['randomUUID']; }): Logger { return { loggerName, loggerMiddlewares, dispatcher, currentTime, randomUUID, }; } /** * Send a logRecord to the log module * * @param logger - the logger * @param params - params needed to create the record */ export function handleRecord(logger: Logger, params: Params): LogRecord { // TODO: simplify this using kind of do notation // TODO: return task instead of triggering effect const begin = Task.Sync.resolve({}); const withUUID = Task.andThen(logger.randomUUID, (uuid) => Task.map(begin, (_) => ({ ..._, uuid, })) ); const withNow = Task.andThen(logger.currentTime, (now) => Task.map(withUUID, (_) => ({ ..._, now, })) ); const initialRecordTask = Task.map(withNow, ({ uuid, now }) => LogRecord({ id: uuid, category: logger.loggerName, created: now, ...params, }) ); const dispatch = Task.andThen(initialRecordTask, (initialRecord) => Task.Sync.tryCall( () => { const logRecord = logger.loggerMiddlewares.reduce( (recordAcc, middleware) => middleware(recordAcc), initialRecord ); logger.dispatcher.handle(logRecord); return logRecord; }, (error: unknown) => {} ) ); return Result.getOrThrow(runTask(dispatch)); } } export type LoggerFunction = { (params: Logger.Params): LogRecord; loggerName: string; loggerMiddlewares: ReadonlyArray; }; export type LoggerParams = Logger.Params & { /** * Add `extraData` to current data * * @param extraData */ withData(extraData: Logger.Params['data']): LoggerParams; }; export namespace LoggerParams { /** * Return a new logger params * * @param data */ export function create({ data, ...other }: Logger.Params): LoggerParams { return { ...other, data, withData(extraData) { return create({ ...other, data: { ...data, ...extraData, }, }); }, }; } } /** * Return a new logger factory function * * @param dispatcher - the dispatcher instance */ export function loggerFactory( dispatcher: LogDispatcher.Module ): (name: LogRecord['category'], middlewares?: ReadonlyArray) => LoggerFunction { function bindLogDispatcher(logger: Logger): LoggerFunction { const log: LoggerFunction = (params) => Logger.handleRecord(logger, params); log.loggerName = logger.loggerName; log.loggerMiddlewares = logger.loggerMiddlewares; return log; } return (loggerName, loggerMiddlewares = []) => bindLogDispatcher( Logger.create({ dispatcher, loggerName, loggerMiddlewares, }) ); } type ReferenceInput = [string, unknown] | { [key: string]: unknown }; /** * Return a function that creates params to be used with a logger function * * @param level - the default message level * @returns a new logger params factory */ export function loggerParamsFactory( level: LogLevel ): ( strings: TemplateStringsArray, ...referencesInput: Array ) => LoggerParams { // eslint-disable-next-line @typescript-eslint/no-explicit-any const emptyArray: any[] = []; function toRefArray(input: ReferenceInput): LogMessage.Ref[] { return Array.isArray(input) ? [LogMessage.Ref(input[0], input[1])] : Object.keys(input).map((name) => LogMessage.Ref(name, input[name])); } return (strings, ...tokens) => { const message = LogMessage( tokens.reduce>( // eslint-disable-next-line @typescript-eslint/no-unsafe-return (acc, token, index) => [ ...acc, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment ...(token === null || token === undefined ? emptyArray : typeof token === 'string' ? [token] : toRefArray(token)), strings[index + 1], ], // eslint-disable-next-line @typescript-eslint/no-non-null-assertion [strings[0]!] ) ); return LoggerParams.create({ levelName: LogLevel.stringify(level), level: LogLevel.value(level), message, data: LogMessage.data(message), }); }; }