This release is 2 versions behind 1.2.2 — the latest version of @logtape/logtape. Jump to latest
Built and signed on GitHub ActionsBuilt and signed on GitHub Actions
Built and signed on GitHub Actions
Simple logging library with zero dependencies for Deno/Node.js/Bun/browsers
This package works with Cloudflare Workers, Node.js, Deno, Bun, Browsers




JSR Score
100%
Published
3 months ago (1.2.0-dev.344+834f24a9)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485import type { ContextLocalStorage } from "./context.ts"; import type { Filter } from "./filter.ts"; import { compareLogLevel, type LogLevel } from "./level.ts"; import type { LogRecord } from "./record.ts"; import type { Sink } from "./sink.ts"; /** * A logger interface. It provides methods to log messages at different * severity levels. * * ```typescript * const logger = getLogger("category"); * logger.trace `A trace message with ${value}` * logger.debug `A debug message with ${value}.`; * logger.info `An info message with ${value}.`; * logger.warn `A warning message with ${value}.`; * logger.error `An error message with ${value}.`; * logger.fatal `A fatal error message with ${value}.`; * ``` */ export interface Logger { /** * The category of the logger. It is an array of strings. */ readonly category: readonly string[]; /** * The logger with the supercategory of the current logger. If the current * logger is the root logger, this is `null`. */ readonly parent: Logger | null; /** * Get a child logger with the given subcategory. * * ```typescript * const logger = getLogger("category"); * const subLogger = logger.getChild("sub-category"); * ``` * * The above code is equivalent to: * * ```typescript * const logger = getLogger("category"); * const subLogger = getLogger(["category", "sub-category"]); * ``` * * @param subcategory The subcategory. * @returns The child logger. */ getChild( subcategory: string | readonly [string] | readonly [string, ...string[]], ): Logger; /** * Get a logger with contextual properties. This is useful for * log multiple messages with the shared set of properties. * * ```typescript * const logger = getLogger("category"); * const ctx = logger.with({ foo: 123, bar: "abc" }); * ctx.info("A message with {foo} and {bar}."); * ctx.warn("Another message with {foo}, {bar}, and {baz}.", { baz: true }); * ``` * * The above code is equivalent to: * * ```typescript * const logger = getLogger("category"); * logger.info("A message with {foo} and {bar}.", { foo: 123, bar: "abc" }); * logger.warn( * "Another message with {foo}, {bar}, and {baz}.", * { foo: 123, bar: "abc", baz: true }, * ); * ``` * * @param properties * @returns * @since 0.5.0 */ with(properties: Record<string, unknown>): Logger; /** * Log a trace message. Use this as a template string prefix. * * ```typescript * logger.trace `A trace message with ${value}.`; * ``` * * @param message The message template strings array. * @param values The message template values. * @since 0.12.0 */ trace(message: TemplateStringsArray, ...values: readonly unknown[]): void; /** * Log a trace message with properties. * * ```typescript * logger.trace('A trace message with {value}.', { value }); * ``` * * If the properties are expensive to compute, you can pass a callback that * returns the properties: * * ```typescript * logger.trace( * 'A trace message with {value}.', * () => ({ value: expensiveComputation() }) * ); * ``` * * @param message The message template. Placeholders to be replaced with * `values` are indicated by keys in curly braces (e.g., * `{value}`). * @param properties The values to replace placeholders with. For lazy * evaluation, this can be a callback that returns the * properties. * @since 0.12.0 */ trace( message: string, properties?: Record<string, unknown> | (() => Record<string, unknown>), ): void; /** * Log a trace values with no message. This is useful when you * want to log properties without a message, e.g., when you want to log * the context of a request or an operation. * * ```typescript * logger.trace({ method: 'GET', url: '/api/v1/resource' }); * ``` * * Note that this is a shorthand for: * * ```typescript * logger.trace('{*}', { method: 'GET', url: '/api/v1/resource' }); * ``` * * If the properties are expensive to compute, you cannot use this shorthand * and should use the following syntax instead: * * ```typescript * logger.trace('{*}', () => ({ * method: expensiveMethod(), * url: expensiveUrl(), * })); * ``` * * @param properties The values to log. Note that this does not take * a callback. * @since 0.12.0 */ trace(properties: Record<string, unknown>): void; /** * Lazily log a trace message. Use this when the message values are expensive * to compute and should only be computed if the message is actually logged. * * ```typescript * logger.trace(l => l`A trace message with ${expensiveValue()}.`); * ``` * * @param callback A callback that returns the message template prefix. * @throws {TypeError} If no log record was made inside the callback. * @since 0.12.0 */ trace(callback: LogCallback): void; /** * Log a debug message. Use this as a template string prefix. * * ```typescript * logger.debug `A debug message with ${value}.`; * ``` * * @param message The message template strings array. * @param values The message template values. */ debug(message: TemplateStringsArray, ...values: readonly unknown[]): void; /** * Log a debug message with properties. * * ```typescript * logger.debug('A debug message with {value}.', { value }); * ``` * * If the properties are expensive to compute, you can pass a callback that * returns the properties: * * ```typescript * logger.debug( * 'A debug message with {value}.', * () => ({ value: expensiveComputation() }) * ); * ``` * * @param message The message template. Placeholders to be replaced with * `values` are indicated by keys in curly braces (e.g., * `{value}`). * @param properties The values to replace placeholders with. For lazy * evaluation, this can be a callback that returns the * properties. */ debug( message: string, properties?: Record<string, unknown> | (() => Record<string, unknown>), ): void; /** * Log a debug values with no message. This is useful when you * want to log properties without a message, e.g., when you want to log * the context of a request or an operation. * * ```typescript * logger.debug({ method: 'GET', url: '/api/v1/resource' }); * ``` * * Note that this is a shorthand for: * * ```typescript * logger.debug('{*}', { method: 'GET', url: '/api/v1/resource' }); * ``` * * If the properties are expensive to compute, you cannot use this shorthand * and should use the following syntax instead: * * ```typescript * logger.debug('{*}', () => ({ * method: expensiveMethod(), * url: expensiveUrl(), * })); * ``` * * @param properties The values to log. Note that this does not take * a callback. * @since 0.11.0 */ debug(properties: Record<string, unknown>): void; /** * Lazily log a debug message. Use this when the message values are expensive * to compute and should only be computed if the message is actually logged. * * ```typescript * logger.debug(l => l`A debug message with ${expensiveValue()}.`); * ``` * * @param callback A callback that returns the message template prefix. * @throws {TypeError} If no log record was made inside the callback. */ debug(callback: LogCallback): void; /** * Log an informational message. Use this as a template string prefix. * * ```typescript * logger.info `An info message with ${value}.`; * ``` * * @param message The message template strings array. * @param values The message template values. */ info(message: TemplateStringsArray, ...values: readonly unknown[]): void; /** * Log an informational message with properties. * * ```typescript * logger.info('An info message with {value}.', { value }); * ``` * * If the properties are expensive to compute, you can pass a callback that * returns the properties: * * ```typescript * logger.info( * 'An info message with {value}.', * () => ({ value: expensiveComputation() }) * ); * ``` * * @param message The message template. Placeholders to be replaced with * `values` are indicated by keys in curly braces (e.g., * `{value}`). * @param properties The values to replace placeholders with. For lazy * evaluation, this can be a callback that returns the * properties. */ info( message: string, properties?: Record<string, unknown> | (() => Record<string, unknown>), ): void; /** * Log an informational values with no message. This is useful when you * want to log properties without a message, e.g., when you want to log * the context of a request or an operation. * * ```typescript * logger.info({ method: 'GET', url: '/api/v1/resource' }); * ``` * * Note that this is a shorthand for: * * ```typescript * logger.info('{*}', { method: 'GET', url: '/api/v1/resource' }); * ``` * * If the properties are expensive to compute, you cannot use this shorthand * and should use the following syntax instead: * * ```typescript * logger.info('{*}', () => ({ * method: expensiveMethod(), * url: expensiveUrl(), * })); * ``` * * @param properties The values to log. Note that this does not take * a callback. * @since 0.11.0 */ info(properties: Record<string, unknown>): void; /** * Lazily log an informational message. Use this when the message values are * expensive to compute and should only be computed if the message is actually * logged. * * ```typescript * logger.info(l => l`An info message with ${expensiveValue()}.`); * ``` * * @param callback A callback that returns the message template prefix. * @throws {TypeError} If no log record was made inside the callback. */ info(callback: LogCallback): void; /** * Log a warning message. Use this as a template string prefix. * * ```typescript * logger.warn `A warning message with ${value}.`; * ``` * * @param message The message template strings array. * @param values The message template values. */ warn(message: TemplateStringsArray, ...values: readonly unknown[]): void; /** * Log a warning message with properties. * * ```typescript * logger.warn('A warning message with {value}.', { value }); * ``` * * If the properties are expensive to compute, you can pass a callback that * returns the properties: * * ```typescript * logger.warn( * 'A warning message with {value}.', * () => ({ value: expensiveComputation() }) * ); * ``` * * @param message The message template. Placeholders to be replaced with * `values` are indicated by keys in curly braces (e.g., * `{value}`). * @param properties The values to replace placeholders with. For lazy * evaluation, this can be a callback that returns the * properties. */ warn( message: string, properties?: Record<string, unknown> | (() => Record<string, unknown>), ): void; /** * Log a warning values with no message. This is useful when you * want to log properties without a message, e.g., when you want to log * the context of a request or an operation. * * ```typescript * logger.warn({ method: 'GET', url: '/api/v1/resource' }); * ``` * * Note that this is a shorthand for: * * ```typescript * logger.warn('{*}', { method: 'GET', url: '/api/v1/resource' }); * ``` * * If the properties are expensive to compute, you cannot use this shorthand * and should use the following syntax instead: * * ```typescript * logger.warn('{*}', () => ({ * method: expensiveMethod(), * url: expensiveUrl(), * })); * ``` * * @param properties The values to log. Note that this does not take * a callback. * @since 0.11.0 */ warn(properties: Record<string, unknown>): void; /** * Lazily log a warning message. Use this when the message values are * expensive to compute and should only be computed if the message is actually * logged. * * ```typescript * logger.warn(l => l`A warning message with ${expensiveValue()}.`); * ``` * * @param callback A callback that returns the message template prefix. * @throws {TypeError} If no log record was made inside the callback. */ warn(callback: LogCallback): void; /** * Log a warning message. Use this as a template string prefix. * * ```typescript * logger.warning `A warning message with ${value}.`; * ``` * * @param message The message template strings array. * @param values The message template values. * @since 0.12.0 */ warning(message: TemplateStringsArray, ...values: readonly unknown[]): void; /** * Log a warning message with properties. * * ```typescript * logger.warning('A warning message with {value}.', { value }); * ``` * * If the properties are expensive to compute, you can pass a callback that * returns the properties: * * ```typescript * logger.warning( * 'A warning message with {value}.', * () => ({ value: expensiveComputation() }) * ); * ``` * * @param message The message template. Placeholders to be replaced with * `values` are indicated by keys in curly braces (e.g., * `{value}`). * @param properties The values to replace placeholders with. For lazy * evaluation, this can be a callback that returns the * properties. * @since 0.12.0 */ warning( message: string, properties?: Record<string, unknown> | (() => Record<string, unknown>), ): void; /** * Log a warning values with no message. This is useful when you * want to log properties without a message, e.g., when you want to log * the context of a request or an operation. * * ```typescript * logger.warning({ method: 'GET', url: '/api/v1/resource' }); * ``` * * Note that this is a shorthand for: * * ```typescript * logger.warning('{*}', { method: 'GET', url: '/api/v1/resource' }); * ``` * * If the properties are expensive to compute, you cannot use this shorthand * and should use the following syntax instead: * * ```typescript * logger.warning('{*}', () => ({ * method: expensiveMethod(), * url: expensiveUrl(), * })); * ``` * * @param properties The values to log. Note that this does not take * a callback. * @since 0.12.0 */ warning(properties: Record<string, unknown>): void; /** * Lazily log a warning message. Use this when the message values are * expensive to compute and should only be computed if the message is actually * logged. * * ```typescript * logger.warning(l => l`A warning message with ${expensiveValue()}.`); * ``` * * @param callback A callback that returns the message template prefix. * @throws {TypeError} If no log record was made inside the callback. * @since 0.12.0 */ warning(callback: LogCallback): void; /** * Log an error message. Use this as a template string prefix. * * ```typescript * logger.error `An error message with ${value}.`; * ``` * * @param message The message template strings array. * @param values The message template values. */ error(message: TemplateStringsArray, ...values: readonly unknown[]): void; /** * Log an error message with properties. * * ```typescript * logger.warn('An error message with {value}.', { value }); * ``` * * If the properties are expensive to compute, you can pass a callback that * returns the properties: * * ```typescript * logger.error( * 'An error message with {value}.', * () => ({ value: expensiveComputation() }) * ); * ``` * * @param message The message template. Placeholders to be replaced with * `values` are indicated by keys in curly braces (e.g., * `{value}`). * @param properties The values to replace placeholders with. For lazy * evaluation, this can be a callback that returns the * properties. */ error( message: string, properties?: Record<string, unknown> | (() => Record<string, unknown>), ): void; /** * Log an error values with no message. This is useful when you * want to log properties without a message, e.g., when you want to log * the context of a request or an operation. * * ```typescript * logger.error({ method: 'GET', url: '/api/v1/resource' }); * ``` * * Note that this is a shorthand for: * * ```typescript * logger.error('{*}', { method: 'GET', url: '/api/v1/resource' }); * ``` * * If the properties are expensive to compute, you cannot use this shorthand * and should use the following syntax instead: * * ```typescript * logger.error('{*}', () => ({ * method: expensiveMethod(), * url: expensiveUrl(), * })); * ``` * * @param properties The values to log. Note that this does not take * a callback. * @since 0.11.0 */ error(properties: Record<string, unknown>): void; /** * Lazily log an error message. Use this when the message values are * expensive to compute and should only be computed if the message is actually * logged. * * ```typescript * logger.error(l => l`An error message with ${expensiveValue()}.`); * ``` * * @param callback A callback that returns the message template prefix. * @throws {TypeError} If no log record was made inside the callback. */ error(callback: LogCallback): void; /** * Log a fatal error message. Use this as a template string prefix. * * ```typescript * logger.fatal `A fatal error message with ${value}.`; * ``` * * @param message The message template strings array. * @param values The message template values. */ fatal(message: TemplateStringsArray, ...values: readonly unknown[]): void; /** * Log a fatal error message with properties. * * ```typescript * logger.warn('A fatal error message with {value}.', { value }); * ``` * * If the properties are expensive to compute, you can pass a callback that * returns the properties: * * ```typescript * logger.fatal( * 'A fatal error message with {value}.', * () => ({ value: expensiveComputation() }) * ); * ``` * * @param message The message template. Placeholders to be replaced with * `values` are indicated by keys in curly braces (e.g., * `{value}`). * @param properties The values to replace placeholders with. For lazy * evaluation, this can be a callback that returns the * properties. */ fatal( message: string, properties?: Record<string, unknown> | (() => Record<string, unknown>), ): void; /** * Log a fatal error values with no message. This is useful when you * want to log properties without a message, e.g., when you want to log * the context of a request or an operation. * * ```typescript * logger.fatal({ method: 'GET', url: '/api/v1/resource' }); * ``` * * Note that this is a shorthand for: * * ```typescript * logger.fatal('{*}', { method: 'GET', url: '/api/v1/resource' }); * ``` * * If the properties are expensive to compute, you cannot use this shorthand * and should use the following syntax instead: * * ```typescript * logger.fatal('{*}', () => ({ * method: expensiveMethod(), * url: expensiveUrl(), * })); * ``` * * @param properties The values to log. Note that this does not take * a callback. * @since 0.11.0 */ fatal(properties: Record<string, unknown>): void; /** * Lazily log a fatal error message. Use this when the message values are * expensive to compute and should only be computed if the message is actually * logged. * * ```typescript * logger.fatal(l => l`A fatal error message with ${expensiveValue()}.`); * ``` * * @param callback A callback that returns the message template prefix. * @throws {TypeError} If no log record was made inside the callback. */ fatal(callback: LogCallback): void; /** * Emits a log record with custom fields while using this logger's * category. * * This is a low-level API for integration scenarios where you need full * control over the log record, particularly for preserving timestamps * from external systems. * * ```typescript * const logger = getLogger(["my-app", "integration"]); * * // Emit a log with a custom timestamp * logger.emit({ * timestamp: kafkaLog.originalTimestamp, * level: "info", * message: [kafkaLog.message], * rawMessage: kafkaLog.message, * properties: { * source: "kafka", * partition: kafkaLog.partition, * offset: kafkaLog.offset, * }, * }); * ``` * * @param record Log record without category field (category comes from * the logger instance) * @since 1.1.0 */ emit(record: Omit<LogRecord, "category">): void; } /** * A logging callback function. It is used to defer the computation of a * message template until it is actually logged. * @param prefix The message template prefix. * @returns The rendered message array. */ export type LogCallback = (prefix: LogTemplatePrefix) => unknown[]; /** * A logging template prefix function. It is used to log a message in * a {@link LogCallback} function. * @param message The message template strings array. * @param values The message template values. * @returns The rendered message array. */ export type LogTemplatePrefix = ( message: TemplateStringsArray, ...values: unknown[] ) => unknown[]; /** * A function type for logging methods in the {@link Logger} interface. * @since 1.0.0 */ export interface LogMethod { /** * Log a message with the given level using a template string. * @param message The message template strings array. * @param values The message template values. */ ( message: TemplateStringsArray, ...values: readonly unknown[] ): void; /** * Log a message with the given level with properties. * @param message The message template. Placeholders to be replaced with * `values` are indicated by keys in curly braces (e.g., * `{value}`). * @param properties The values to replace placeholders with. For lazy * evaluation, this can be a callback that returns the * properties. */ ( message: string, properties?: Record<string, unknown> | (() => Record<string, unknown>), ): void; /** * Log a message with the given level with no message. * @param properties The values to log. Note that this does not take * a callback. */ (properties: Record<string, unknown>): void; /** * Lazily log a message with the given level. * @param callback A callback that returns the message template prefix. * @throws {TypeError} If no log record was made inside the callback. */ (callback: LogCallback): void; } /** * Get a logger with the given category. * * ```typescript * const logger = getLogger(["my-app"]); * ``` * * @param category The category of the logger. It can be a string or an array * of strings. If it is a string, it is equivalent to an array * with a single element. * @returns The logger. */ export function getLogger(category: string | readonly string[] = []): Logger { return LoggerImpl.getLogger(category); } /** * The symbol for the global root logger. */ const globalRootLoggerSymbol = Symbol.for("logtape.rootLogger"); /** * The global root logger registry. */ interface GlobalRootLoggerRegistry { [globalRootLoggerSymbol]?: LoggerImpl; } /** * A logger implementation. Do not use this directly; use {@link getLogger} * instead. This class is exported for testing purposes. */ export class LoggerImpl implements Logger { readonly parent: LoggerImpl | null; readonly children: Record<string, LoggerImpl | WeakRef<LoggerImpl>>; readonly category: readonly string[]; readonly sinks: Sink[]; parentSinks: "inherit" | "override" = "inherit"; readonly filters: Filter[]; lowestLevel: LogLevel | null = "trace"; contextLocalStorage?: ContextLocalStorage<Record<string, unknown>>; static getLogger(category: string | readonly string[] = []): LoggerImpl { let rootLogger: LoggerImpl | null = globalRootLoggerSymbol in globalThis ? ((globalThis as GlobalRootLoggerRegistry)[globalRootLoggerSymbol] ?? null) : null; if (rootLogger == null) { rootLogger = new LoggerImpl(null, []); (globalThis as GlobalRootLoggerRegistry)[globalRootLoggerSymbol] = rootLogger; } if (typeof category === "string") return rootLogger.getChild(category); if (category.length === 0) return rootLogger; return rootLogger.getChild(category as readonly [string, ...string[]]); } private constructor(parent: LoggerImpl | null, category: readonly string[]) { this.parent = parent; this.children = {}; this.category = category; this.sinks = []; this.filters = []; } getChild( subcategory: | string | readonly [string] | readonly [string, ...(readonly string[])], ): LoggerImpl { const name = typeof subcategory === "string" ? subcategory : subcategory[0]; const childRef = this.children[name]; let child: LoggerImpl | undefined = childRef instanceof LoggerImpl ? childRef : childRef?.deref(); if (child == null) { child = new LoggerImpl(this, [...this.category, name]); this.children[name] = "WeakRef" in globalThis ? new WeakRef(child) : child; } if (typeof subcategory === "string" || subcategory.length === 1) { return child; } return child.getChild( subcategory.slice(1) as [string, ...(readonly string[])], ); } /** * Reset the logger. This removes all sinks and filters from the logger. */ reset(): void { while (this.sinks.length > 0) this.sinks.shift(); this.parentSinks = "inherit"; while (this.filters.length > 0) this.filters.shift(); this.lowestLevel = "trace"; } /** * Reset the logger and all its descendants. This removes all sinks and * filters from the logger and all its descendants. */ resetDescendants(): void { for (const child of Object.values(this.children)) { const logger = child instanceof LoggerImpl ? child : child.deref(); if (logger != null) logger.resetDescendants(); } this.reset(); } with(properties: Record<string, unknown>): Logger { return new LoggerCtx(this, { ...properties }); } filter(record: LogRecord): boolean { for (const filter of this.filters) { if (!filter(record)) return false; } if (this.filters.length < 1) return this.parent?.filter(record) ?? true; return true; } *getSinks(level: LogLevel): Iterable<Sink> { if ( this.lowestLevel === null || compareLogLevel(level, this.lowestLevel) < 0 ) { return; } if (this.parent != null && this.parentSinks === "inherit") { for (const sink of this.parent.getSinks(level)) yield sink; } for (const sink of this.sinks) yield sink; } emit(record: Omit<LogRecord, "category">): void; emit(record: LogRecord, bypassSinks?: Set<Sink>): void; emit( record: Omit<LogRecord, "category"> | LogRecord, bypassSinks?: Set<Sink>, ): void { const fullRecord: LogRecord = "category" in record ? record as LogRecord : { ...record, category: this.category }; if ( this.lowestLevel === null || compareLogLevel(fullRecord.level, this.lowestLevel) < 0 || !this.filter(fullRecord) ) { return; } for (const sink of this.getSinks(fullRecord.level)) { if (bypassSinks?.has(sink)) continue; try { sink(fullRecord); } catch (error) { const bypassSinks2 = new Set(bypassSinks); bypassSinks2.add(sink); metaLogger.log( "fatal", "Failed to emit a log record to sink {sink}: {error}", { sink, error, record: fullRecord }, bypassSinks2, ); } } } log( level: LogLevel, rawMessage: string, properties: Record<string, unknown> | (() => Record<string, unknown>), bypassSinks?: Set<Sink>, ): void { const implicitContext = LoggerImpl.getLogger().contextLocalStorage?.getStore() ?? {}; let cachedProps: Record<string, unknown> | undefined = undefined; const record: LogRecord = typeof properties === "function" ? { category: this.category, level, timestamp: Date.now(), get message() { return parseMessageTemplate(rawMessage, this.properties); }, rawMessage, get properties() { if (cachedProps == null) { cachedProps = { ...implicitContext, ...properties(), }; } return cachedProps; }, } : { category: this.category, level, timestamp: Date.now(), message: parseMessageTemplate(rawMessage, { ...implicitContext, ...properties, }), rawMessage, properties: { ...implicitContext, ...properties }, }; this.emit(record, bypassSinks); } logLazily( level: LogLevel, callback: LogCallback, properties: Record<string, unknown> = {}, ): void { const implicitContext = LoggerImpl.getLogger().contextLocalStorage?.getStore() ?? {}; let rawMessage: TemplateStringsArray | undefined = undefined; let msg: unknown[] | undefined = undefined; function realizeMessage(): [unknown[], TemplateStringsArray] { if (msg == null || rawMessage == null) { msg = callback((tpl, ...values) => { rawMessage = tpl; return renderMessage(tpl, values); }); if (rawMessage == null) throw new TypeError("No log record was made."); } return [msg, rawMessage]; } this.emit({ category: this.category, level, get message() { return realizeMessage()[0]; }, get rawMessage() { return realizeMessage()[1]; }, timestamp: Date.now(), properties: { ...implicitContext, ...properties }, }); } logTemplate( level: LogLevel, messageTemplate: TemplateStringsArray, values: unknown[], properties: Record<string, unknown> = {}, ): void { const implicitContext = LoggerImpl.getLogger().contextLocalStorage?.getStore() ?? {}; this.emit({ category: this.category, level, message: renderMessage(messageTemplate, values), rawMessage: messageTemplate, timestamp: Date.now(), properties: { ...implicitContext, ...properties }, }); } trace( message: | TemplateStringsArray | string | LogCallback | Record<string, unknown>, ...values: unknown[] ): void { if (typeof message === "string") { this.log("trace", message, (values[0] ?? {}) as Record<string, unknown>); } else if (typeof message === "function") { this.logLazily("trace", message); } else if (!Array.isArray(message)) { this.log("trace", "{*}", message as Record<string, unknown>); } else { this.logTemplate("trace", message as TemplateStringsArray, values); } } debug( message: | TemplateStringsArray | string | LogCallback | Record<string, unknown>, ...values: unknown[] ): void { if (typeof message === "string") { this.log("debug", message, (values[0] ?? {}) as Record<string, unknown>); } else if (typeof message === "function") { this.logLazily("debug", message); } else if (!Array.isArray(message)) { this.log("debug", "{*}", message as Record<string, unknown>); } else { this.logTemplate("debug", message as TemplateStringsArray, values); } } info( message: | TemplateStringsArray | string | LogCallback | Record<string, unknown>, ...values: unknown[] ): void { if (typeof message === "string") { this.log("info", message, (values[0] ?? {}) as Record<string, unknown>); } else if (typeof message === "function") { this.logLazily("info", message); } else if (!Array.isArray(message)) { this.log("info", "{*}", message as Record<string, unknown>); } else { this.logTemplate("info", message as TemplateStringsArray, values); } } warn( message: | TemplateStringsArray | string | LogCallback | Record<string, unknown>, ...values: unknown[] ): void { if (typeof message === "string") { this.log( "warning", message, (values[0] ?? {}) as Record<string, unknown>, ); } else if (typeof message === "function") { this.logLazily("warning", message); } else if (!Array.isArray(message)) { this.log("warning", "{*}", message as Record<string, unknown>); } else { this.logTemplate("warning", message as TemplateStringsArray, values); } } warning( message: | TemplateStringsArray | string | LogCallback | Record<string, unknown>, ...values: unknown[] ): void { this.warn(message, ...values); } error( message: | TemplateStringsArray | string | LogCallback | Record<string, unknown>, ...values: unknown[] ): void { if (typeof message === "string") { this.log("error", message, (values[0] ?? {}) as Record<string, unknown>); } else if (typeof message === "function") { this.logLazily("error", message); } else if (!Array.isArray(message)) { this.log("error", "{*}", message as Record<string, unknown>); } else { this.logTemplate("error", message as TemplateStringsArray, values); } } fatal( message: | TemplateStringsArray | string | LogCallback | Record<string, unknown>, ...values: unknown[] ): void { if (typeof message === "string") { this.log("fatal", message, (values[0] ?? {}) as Record<string, unknown>); } else if (typeof message === "function") { this.logLazily("fatal", message); } else if (!Array.isArray(message)) { this.log("fatal", "{*}", message as Record<string, unknown>); } else { this.logTemplate("fatal", message as TemplateStringsArray, values); } } } /** * A logger implementation with contextual properties. Do not use this * directly; use {@link Logger.with} instead. This class is exported * for testing purposes. */ export class LoggerCtx implements Logger { logger: LoggerImpl; properties: Record<string, unknown>; constructor(logger: LoggerImpl, properties: Record<string, unknown>) { this.logger = logger; this.properties = properties; } get category(): readonly string[] { return this.logger.category; } get parent(): Logger | null { return this.logger.parent; } getChild( subcategory: string | readonly [string] | readonly [string, ...string[]], ): Logger { return this.logger.getChild(subcategory).with(this.properties); } with(properties: Record<string, unknown>): Logger { return new LoggerCtx(this.logger, { ...this.properties, ...properties }); } log( level: LogLevel, message: string, properties: Record<string, unknown> | (() => Record<string, unknown>), bypassSinks?: Set<Sink>, ): void { this.logger.log( level, message, typeof properties === "function" ? () => ({ ...this.properties, ...properties(), }) : { ...this.properties, ...properties }, bypassSinks, ); } logLazily(level: LogLevel, callback: LogCallback): void { this.logger.logLazily(level, callback, this.properties); } logTemplate( level: LogLevel, messageTemplate: TemplateStringsArray, values: unknown[], ): void { this.logger.logTemplate(level, messageTemplate, values, this.properties); } emit(record: Omit<LogRecord, "category">): void { const recordWithContext = { ...record, properties: { ...this.properties, ...record.properties }, }; this.logger.emit(recordWithContext); } trace( message: | TemplateStringsArray | string | LogCallback | Record<string, unknown>, ...values: unknown[] ): void { if (typeof message === "string") { this.log("trace", message, (values[0] ?? {}) as Record<string, unknown>); } else if (typeof message === "function") { this.logLazily("trace", message); } else if (!Array.isArray(message)) { this.log("trace", "{*}", message as Record<string, unknown>); } else { this.logTemplate("trace", message as TemplateStringsArray, values); } } debug( message: | TemplateStringsArray | string | LogCallback | Record<string, unknown>, ...values: unknown[] ): void { if (typeof message === "string") { this.log("debug", message, (values[0] ?? {}) as Record<string, unknown>); } else if (typeof message === "function") { this.logLazily("debug", message); } else if (!Array.isArray(message)) { this.log("debug", "{*}", message as Record<string, unknown>); } else { this.logTemplate("debug", message as TemplateStringsArray, values); } } info( message: | TemplateStringsArray | string | LogCallback | Record<string, unknown>, ...values: unknown[] ): void { if (typeof message === "string") { this.log("info", message, (values[0] ?? {}) as Record<string, unknown>); } else if (typeof message === "function") { this.logLazily("info", message); } else if (!Array.isArray(message)) { this.log("info", "{*}", message as Record<string, unknown>); } else { this.logTemplate("info", message as TemplateStringsArray, values); } } warn( message: | TemplateStringsArray | string | LogCallback | Record<string, unknown>, ...values: unknown[] ): void { if (typeof message === "string") { this.log( "warning", message, (values[0] ?? {}) as Record<string, unknown>, ); } else if (typeof message === "function") { this.logLazily("warning", message); } else if (!Array.isArray(message)) { this.log("warning", "{*}", message as Record<string, unknown>); } else { this.logTemplate("warning", message as TemplateStringsArray, values); } } warning( message: | TemplateStringsArray | string | LogCallback | Record<string, unknown>, ...values: unknown[] ): void { this.warn(message, ...values); } error( message: | TemplateStringsArray | string | LogCallback | Record<string, unknown>, ...values: unknown[] ): void { if (typeof message === "string") { this.log("error", message, (values[0] ?? {}) as Record<string, unknown>); } else if (typeof message === "function") { this.logLazily("error", message); } else if (!Array.isArray(message)) { this.log("error", "{*}", message as Record<string, unknown>); } else { this.logTemplate("error", message as TemplateStringsArray, values); } } fatal( message: | TemplateStringsArray | string | LogCallback | Record<string, unknown>, ...values: unknown[] ): void { if (typeof message === "string") { this.log("fatal", message, (values[0] ?? {}) as Record<string, unknown>); } else if (typeof message === "function") { this.logLazily("fatal", message); } else if (!Array.isArray(message)) { this.log("fatal", "{*}", message as Record<string, unknown>); } else { this.logTemplate("fatal", message as TemplateStringsArray, values); } } } /** * The meta logger. It is a logger with the category `["logtape", "meta"]`. */ const metaLogger = LoggerImpl.getLogger(["logtape", "meta"]); /** * Parse a message template into a message template array and a values array. * @param template The message template. * @param properties The values to replace placeholders with. * @returns The message template array and the values array. */ export function parseMessageTemplate( template: string, properties: Record<string, unknown>, ): readonly unknown[] { const length = template.length; if (length === 0) return [""]; // Fast path: no placeholders if (!template.includes("{")) return [template]; const message: unknown[] = []; let startIndex = 0; for (let i = 0; i < length; i++) { const char = template[i]; if (char === "{") { const nextChar = i + 1 < length ? template[i + 1] : ""; if (nextChar === "{") { // Escaped { character - skip and continue i++; // Skip the next { continue; } // Find the closing } const closeIndex = template.indexOf("}", i + 1); if (closeIndex === -1) { // No closing } found, treat as literal text continue; } // Add text before placeholder const beforeText = template.slice(startIndex, i); message.push(beforeText.replace(/{{/g, "{").replace(/}}/g, "}")); // Extract and process placeholder key const key = template.slice(i + 1, closeIndex); // Resolve property value let prop: unknown; // Check for wildcard patterns const trimmedKey = key.trim(); if (trimmedKey === "*") { // This is a wildcard pattern prop = key in properties ? properties[key] : "*" in properties ? properties["*"] : properties; } else { // Regular property lookup with possible whitespace handling if (key !== trimmedKey) { // Key has leading/trailing whitespace prop = key in properties ? properties[key] : properties[trimmedKey]; } else { // Key has no leading/trailing whitespace prop = properties[key]; } } message.push(prop); i = closeIndex; // Move to the } startIndex = i + 1; } else if (char === "}" && i + 1 < length && template[i + 1] === "}") { // Escaped } character - skip i++; // Skip the next } } } // Add remaining text const remainingText = template.slice(startIndex); message.push(remainingText.replace(/{{/g, "{").replace(/}}/g, "}")); return message; } /** * Render a message template with values. * @param template The message template. * @param values The message template values. * @returns The message template values interleaved between the substitution * values. */ export function renderMessage( template: TemplateStringsArray, values: readonly unknown[], ): unknown[] { const args = []; for (let i = 0; i < template.length; i++) { args.push(template[i]); if (i < values.length) args.push(values[i]); } return args; }