UNPKG

studiocms

Version:

Astro Native CMS for AstroDB. Built from the ground up by the Astro community.

270 lines (244 loc) 8.95 kB
import { dual, Effect, List, Logger, LogLevel, pipe, type Utils } from '@withstudiocms/effect'; import chalk from 'chalk'; export function stripNameFromLabel(label: string): string { const prefix = 'studiocms/'; return label.startsWith(prefix) ? label.slice(prefix.length) : label; } /** * A cache that stores instances of `AstroIntegrationLogger` associated with their string keys. * This is used to avoid creating multiple logger instances for the same key. */ export const loggerCache = new Map<string, S48Logger>(); export type LoggerLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'; // same as Pino export const levels: Record<LoggerLevel, number> = { debug: 20, info: 30, warn: 40, error: 50, silent: 90, }; export interface LogOptions { level: LoggerLevel; } const dateTimeFormat = new Intl.DateTimeFormat([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }); export const getEventPrefix = (level: LoggerLevel, label?: string) => { const timestamp = `${dateTimeFormat.format(/* @__PURE__ */ new Date())}`; const prefix = []; if (level === 'error' || level === 'warn' || level === 'debug') { prefix.push(chalk.bold(timestamp)); prefix.push(`[${level.toUpperCase()}]`); } else { prefix.push(timestamp); } if (label) { prefix.push(`[${label}]`); } if (level === 'error') { return chalk.red(prefix.join(' ')); } if (level === 'warn') { return chalk.yellow(prefix.join(' ')); } if (level === 'debug') { return chalk.blue(prefix.join(' ')); } if (prefix.length === 1) { /* v8 ignore start */ return chalk.dim(prefix[0]); /* v8 ignore stop */ } return `${chalk.dim(prefix[0])} ${chalk.blue(prefix.splice(1).join(' '))}`; }; export class S48Logger { options: LogOptions; label: string; constructor(logging: LogOptions, label: string) { this.options = logging; this.label = label; } /** * Creates a new logger instance with a new label, but the same log options. */ fork(label: string): S48Logger { return new S48Logger(this.options, label); } info(message: string) { console.log(`${getEventPrefix('info', this.label)} ${message}`); } warn(message: string) { console.warn(`${getEventPrefix('warn', this.label)} ${message}`); } error(message: string) { console.error(`${getEventPrefix('error', this.label)} ${message}`); } debug(message: string) { console.debug(`${getEventPrefix('debug', this.label)} ${message}`); } } const _logger = new S48Logger({ level: 'info' }, 'studiocms:runtime'); /** * Creates a logger instance with a specific label for categorizing log messages. * * @param label - A string used to label the logger instance. This label is appended * to the logger's namespace to help identify the source of log messages. * * @returns A logger function that processes log messages based on their log level. * The logger supports the following log levels: * - `LogLevel.Trace` and `LogLevel.Debug`: Logs messages as debug. * - `LogLevel.Error` and `LogLevel.Fatal`: Logs messages as errors. * - `LogLevel.Warning`: Logs messages as warnings. * - `LogLevel.All`, `LogLevel.Info`, and `LogLevel.None`: Logs messages as info. * - Any other log level defaults to logging messages as debug. * @internal */ export const makeLogger = (label: string) => Logger.make(({ logLevel, message: _message, spans }) => { const logger = loggerCache.get(label) ?? _logger.fork(`studiocms:runtime/${stripNameFromLabel(label)}`); loggerCache.set(label, logger); const list = List.toArray(spans); const spanPart = list.length ? ` :: ${list.join(' › ')}` : ''; const message = `${String(_message)}${spanPart}`; /* v8 ignore start */ switch (logLevel) { case LogLevel.Trace: case LogLevel.Debug: { logger.debug(message); break; } case LogLevel.Error: case LogLevel.Fatal: { logger.error(message); break; } case LogLevel.Warning: { logger.warn(message); break; } case LogLevel.All: case LogLevel.Info: { logger.info(message); break; } default: { logger.info(message); } } /* v8 ignore stop */ }); const sysLogLevel = process.env.STUDIOCMS_LOGLEVEL as | 'All' | 'Fatal' | 'Error' | 'Warning' | 'Info' | 'Debug' | 'Trace' | 'None' | undefined; /** * Sets the minimum log level for the logger based on the provided configuration. * * @remarks * This function utilizes the `Logger.withMinimumLogLevel` method to adjust the logging level dynamically. * The log level is determined by converting the `config.logLevel` value using the `fromLiteral` utility. * * @param config.logLevel - The log level specified in the configuration, which is converted to the appropriate format. * @internal */ const level = LogLevel.fromLiteral(sysLogLevel ?? 'Info'); const setLoggerLevel = Logger.withMinimumLogLevel(level); /** * Sets a custom logger for the Astro Runtime by replacing the default logger with the * runtime AstroIntegrationLogger configured with the specified label. * * @param label - A string used to label the logger, providing context for log messages. * @returns An effect that provides the custom logger to the application. * @internal */ const setLogger = (label: string) => Effect.provide(Logger.replace(Logger.defaultLogger, makeLogger(label))); /** * Creates a runtime logger effect transformer that applies a specific label to log messages * and configures the logging behavior based on the provided log level. * * @param label - A string label to associate with the logger for identifying log messages. * @returns A higher-order function that takes an `Effect` and returns a new `Effect` with * the logger configuration applied. */ export const runtimeLogger = dual< (label: string) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>, <A, E, R>(self: Effect.Effect<A, E, R>, label: string) => Effect.Effect<A, E, R> >(2, (self, label) => pipe(self, setLoggerLevel, setLogger(label))); /** * Wraps an `Effect` with additional logging functionality. * * This function applies a runtime logger and a log span to the provided `Effect`, * enabling detailed logging for debugging and monitoring purposes. * * @param label - A string label used to identify the log entries and span. * @param effect - The `Effect` to be wrapped with logging functionality. * * @returns A new `Effect` that includes runtime logging and a log span. */ export const pipeLogger = dual< (label: string) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>, <A, E, R>(effect: Effect.Effect<A, E, R>, label: string) => Effect.Effect<A, E, R> >(2, (effect, label) => pipe(effect, runtimeLogger(label), Effect.withLogSpan(`span-${label}`))); /** * Generates a logger function that wraps an effectful generator function with logging capabilities. * * @param label - A string label used to identify the logger. * @returns A function that takes a generator function `f` and returns an `Effect.Effect` instance. * * The returned function accepts a generator function `f` that yields wrapped effects (`YieldWrap<Effect.Effect>`). * It logs the execution of the generator function and its effects using the provided label. * * @typeParam Eff - The type of the yielded effects within the generator function. * @typeParam AEff - The return type of the generator function. * * The resulting `Effect.Effect` has the following type parameters: * - `AEff`: The return type of the generator function. * - `E`: The error type inferred from the yielded effects, or `never` if no effects are yielded. * - `R`: The environment type inferred from the yielded effects, or `never` if no effects are yielded. */ export function genLogger(label: string) { // biome-ignore lint/suspicious/noExplicitAny: this is a valid use case for explicit any. return <Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, AEff>( f: (resume: Effect.Adapter) => Generator<Eff, AEff, never> ): Effect.Effect< AEff, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Utils.YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R : never > => pipeLogger(label)(Effect.gen(f)); } // // Testing Examples // const program1 = Effect.gen(function* () { // yield* Effect.log('start'); // // yield* task1; // // yield* task2; // yield* Effect.log('done'); // return true; // }).pipe(Effect.withLogSpan('span-program1')); // const test1 = await Effect.runPromise(program1.pipe(runtimeLogger('test'))); // const program2 = genLogger('program2')(function* () { // yield* Effect.log('start'); // // yield* task1; // // yield* task2; // yield* Effect.log('done'); // return true; // }); // const test2 = await Effect.runPromise(program2);