Built and signed on GitHub ActionsBuilt and signed on GitHub Actions
Built and signed on GitHub Actions
latest
dahlia/logtapeSimple 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
2 months ago (0.8.0)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300import type { ContextLocalStorage } from "./context.ts"; import { type FilterLike, toFilter } from "./filter.ts"; import type { LogLevel } from "./level.ts"; import { LoggerImpl } from "./logger.ts"; import { getConsoleSink, type Sink } from "./sink.ts"; /** * A configuration for the loggers. */ export interface Config<TSinkId extends string, TFilterId extends string> { /** * The sinks to use. The keys are the sink identifiers, and the values are * {@link Sink}s. */ sinks: Record<TSinkId, Sink>; /** * The filters to use. The keys are the filter identifiers, and the values * are either {@link Filter}s or {@link LogLevel}s. */ filters?: Record<TFilterId, FilterLike>; /** * The loggers to configure. */ loggers: LoggerConfig<TSinkId, TFilterId>[]; /** * The context-local storage to use for implicit contexts. * @since 0.7.0 */ contextLocalStorage?: ContextLocalStorage<Record<string, unknown>>; /** * Whether to reset the configuration before applying this one. */ reset?: boolean; } /** * A logger configuration. */ export interface LoggerConfig< TSinkId extends string, TFilterId extends string, > { /** * The category of the logger. If a string, it is equivalent to an array * with one element. */ category: string | string[]; /** * The sink identifiers to use. */ sinks?: TSinkId[]; /** * Whether to inherit the parent's sinks. If `inherit`, the parent's sinks * are used along with the specified sinks. If `override`, the parent's * sinks are not used, and only the specified sinks are used. * * The default is `inherit`. * @default `"inherit" * @since 0.6.0 */ parentSinks?: "inherit" | "override"; /** * The filter identifiers to use. */ filters?: TFilterId[]; /** * The log level to filter by. If `null`, the logger will reject all * records. * @deprecated Use `filters` instead for backward compatibility, or use * `lowestLevel` for less-misleading behavior. */ level?: LogLevel | null; /** * The lowest log level to accept. If `null`, the logger will reject all * records. * @since 0.8.0 */ lowestLevel?: LogLevel | null; } /** * The current configuration, if any. Otherwise, `null`. */ let currentConfig: Config<string, string> | null = null; /** * Strong references to the loggers. * This is to prevent the loggers from being garbage collected so that their * sinks and filters are not removed. */ const strongRefs: Set<LoggerImpl> = new Set(); /** * Disposables to dispose when resetting the configuration. */ const disposables: Set<Disposable> = new Set(); /** * Async disposables to dispose when resetting the configuration. */ const asyncDisposables: Set<AsyncDisposable> = new Set(); /** * Configure the loggers with the specified configuration. * * Note that if the given sinks or filters are disposable, they will be * disposed when the configuration is reset, or when the process exits. * * @example * ```typescript * await configure({ * sinks: { * console: getConsoleSink(), * }, * filters: { * slow: (log) => * "duration" in log.properties && * log.properties.duration as number > 1000, * }, * loggers: [ * { * category: "my-app", * sinks: ["console"], * level: "info", * }, * { * category: ["my-app", "sql"], * filters: ["slow"], * level: "debug", * }, * { * category: "logtape", * sinks: ["console"], * level: "error", * }, * ], * }); * ``` * * @param config The configuration. */ export async function configure< TSinkId extends string, TFilterId extends string, >(config: Config<TSinkId, TFilterId>): Promise<void> { if (currentConfig != null && !config.reset) { throw new ConfigError( "Already configured; if you want to reset, turn on the reset flag.", ); } await reset(); currentConfig = config; let metaConfigured = false; let levelUsed = false; for (const cfg of config.loggers) { if ( cfg.category.length === 0 || (cfg.category.length === 1 && cfg.category[0] === "logtape") || (cfg.category.length === 2 && cfg.category[0] === "logtape" && cfg.category[1] === "meta") ) { metaConfigured = true; } const logger = LoggerImpl.getLogger(cfg.category); for (const sinkId of cfg.sinks ?? []) { const sink = config.sinks[sinkId]; if (!sink) { await reset(); throw new ConfigError(`Sink not found: ${sinkId}.`); } logger.sinks.push(sink); } logger.parentSinks = cfg.parentSinks ?? "inherit"; if (cfg.lowestLevel !== undefined) { logger.lowestLevel = cfg.lowestLevel; } if (cfg.level !== undefined) { levelUsed = true; logger.filters.push(toFilter(cfg.level)); } for (const filterId of cfg.filters ?? []) { const filter = config.filters?.[filterId]; if (filter === undefined) { await reset(); throw new ConfigError(`Filter not found: ${filterId}.`); } logger.filters.push(toFilter(filter)); } strongRefs.add(logger); } LoggerImpl.getLogger().contextLocalStorage = config.contextLocalStorage; for (const sink of Object.values<Sink>(config.sinks)) { if (Symbol.asyncDispose in sink) { asyncDisposables.add(sink as AsyncDisposable); } if (Symbol.dispose in sink) disposables.add(sink as Disposable); } for (const filter of Object.values<FilterLike>(config.filters ?? {})) { if (filter == null || typeof filter === "string") continue; if (Symbol.asyncDispose in filter) { asyncDisposables.add(filter as AsyncDisposable); } if (Symbol.dispose in filter) disposables.add(filter as Disposable); } if ("process" in globalThis && !("Deno" in globalThis)) { // @ts-ignore: It's fine to use process in Node // deno-lint-ignore no-process-globals process.on("exit", dispose); } else { // @ts-ignore: It's fine to addEventListener() on the browser/Deno addEventListener("unload", dispose); } const meta = LoggerImpl.getLogger(["logtape", "meta"]); if (!metaConfigured) { meta.sinks.push(getConsoleSink()); } meta.info( "LogTape loggers are configured. Note that LogTape itself uses the meta " + "logger, which has category {metaLoggerCategory}. The meta logger " + "purposes to log internal errors such as sink exceptions. If you " + "are seeing this message, the meta logger is somehow configured. " + "It's recommended to configure the meta logger with a separate sink " + "so that you can easily notice if logging itself fails or is " + "misconfigured. To turn off this message, configure the meta logger " + "with higher log levels than {dismissLevel}. See also " + "<https://logtape.org/manual/categories#meta-logger>.", { metaLoggerCategory: ["logtape", "meta"], dismissLevel: "info" }, ); if (levelUsed) { meta.warn( "The level option is deprecated in favor of lowestLevel option. " + "Please update your configuration. See also " + "<https://logtape.org/manual/levels#configuring-severity-levels>.", ); } } /** * Get the current configuration, if any. Otherwise, `null`. * @returns The current configuration, if any. Otherwise, `null`. */ export function getConfig(): Config<string, string> | null { return currentConfig; } /** * Reset the configuration. Mostly for testing purposes. */ export async function reset(): Promise<void> { await dispose(); const rootLogger = LoggerImpl.getLogger([]); rootLogger.resetDescendants(); delete rootLogger.contextLocalStorage; strongRefs.clear(); currentConfig = null; } /** * Dispose of the disposables. */ export async function dispose(): Promise<void> { for (const disposable of disposables) disposable[Symbol.dispose](); disposables.clear(); const promises: PromiseLike<void>[] = []; for (const disposable of asyncDisposables) { promises.push(disposable[Symbol.asyncDispose]()); asyncDisposables.delete(disposable); } await Promise.all(promises); } /** * A configuration error. */ export class ConfigError extends Error { /** * Constructs a new configuration error. * @param message The error message. */ constructor(message: string) { super(message); this.name = "ConfigureError"; } }