/** * Log levels in order of verbosity (least to most) * - none: Silent * - error: Critical failures that prevent operation * - warn: Issues that may cause problems but don't stop execution * - info: High-level informational messages (default) * - debug: Detailed debugging information (maintainer level) * - trace: Very detailed tracing — sets `internal: true` on context * @public */ export type LogLevel = "none" | "error" | "warn" | "info" | "debug"; /** * Namespaces organize logs by functional domain. * @internal */ export type LogNamespace = string; type LogContext = { [key: string]: unknown }; /** * @public */ export interface Logger { error: (message: string, context?: LogContext) => void; warn: (message: string, context?: LogContext) => void; info: (message: string, context?: LogContext) => void; debug: (message: string, context?: LogContext) => void; child: (domain: string, context?: LogContext) => Logger; } const LEVELS: readonly LogLevel[] = ["none", "error", "warn", "info", "debug"]; interface LoggerOptions { namespace?: LogNamespace; context?: LogContext; logLevel?: LogLevel; } /** * Creates a leveled logger with an optional namespace prefix and bound * context. Calls below the configured `logLevel` are suppressed; `"none"` * silences the logger entirely. * * Use {@link Logger.child} to derive a sub-logger with an extended namespace * (e.g. `parent:domain`) that inherits the parent's level and merges its * bound context. * * @param options - Logger configuration. * @param options.namespace - Prepended to every message in `[brackets]`. * @param options.context - Bound context merged with per-call context. * Per-call keys win on conflict. * @param options.logLevel - Maximum verbosity to emit. Default `"info"`. * * @example * ```ts * const log = createLogger({ namespace: "checkout", logLevel: "debug" }); * log.info("placed", { orderId: "abc" }); * // → [checkout] placed { orderId: "abc" } * * const auth = log.child("auth", { tenant: "acme" }); * auth.warn("token expiring"); * // → [checkout:auth] token expiring { tenant: "acme" } * ``` * * @public */ export function createLogger({ namespace, context: baseContext, logLevel = "info", }: LoggerOptions = {}): Logger { function isLevelEnabled(level: LogLevel): boolean { return LEVELS.indexOf(level) <= LEVELS.indexOf(logLevel); } function logAtLevel( level: LogLevel, message: string, context?: LogContext, ): void { if (!isLevelEnabled(level)) return; const merged = (baseContext ?? context) ? { ...baseContext, ...context } : undefined; const args: unknown[] = [ ...(namespace ? [`[${namespace}]`] : []), message, ...(merged ? [merged] : []), ]; if (level === "error") console.error(...args); else if (level === "warn") console.warn(...args); // oxlint-disable-next-line no-console else if (level === "info") console.info(...args); // oxlint-disable-next-line no-console else console.debug(...args); } return { error: (message, context) => logAtLevel("error", message, context), warn: (message, context) => logAtLevel("warn", message, context), info: (message, context) => logAtLevel("info", message, context), debug: (message, context) => logAtLevel("debug", message, context), child: (domain, context) => createLogger({ logLevel, namespace: namespace ? `${namespace}:${domain}` : domain, context: baseContext || context ? { ...baseContext, ...context } : undefined, }), }; } /** * Shared workbench logger instance. Use this from both the workbench host * and its remotes so lifecycle and diagnostic logs appear under a single * namespace. * * @public */ export const logger: Logger = createLogger({ namespace: "sanity-workbench", logLevel: "debug", });