Repository: oclif/core
Pull request #1066 (2024-04-23 14:43:58)

feat: customizable logger
## Customizable Logger

In the current major version, we exclusively use [debug](https://www.npmjs.com/package/debug) for debug logs. This PR exports a `Logger` interface that will allow people to provide a custom logger for `@oclif/core` to use. This will be useful if you want all the `@oclif/core` debug logs to go through your own logger.

The default logger will continue to use `debug` under the hood. So if you choose to use the default, you can continue to use the `DEBUG` environment variable to access the debug logs in the console. The only breaking change will be that the namespace for all the logs with be prefixed with a root namespace, `oclif`.

So if you're used to using `DEBUG=config:* my-cli do stuff`, you'll need to start doing this instead: `DEBUG=oclif:config:* my-cli do stuff`


### Interface

```typescript
export type Logger = {
  debug: (formatter: unknown, ...args: unknown[]) => void
  error: (formatter: unknown, ...args: unknown[]) => void
  info: (formatter: unknown, ...args: unknown[]) => void
  trace: (formatter: unknown, ...args: unknown[]) => void
  warn: (formatter: unknown, ...args: unknown[]) => void
  child: (namespace: string) => Logger
  namespace: string
}
```

### Usage

```typescript
// oclif-logger.ts
import { format } from 'node:util';
import { Interfaces } from '@oclif/core';
import { Logger } from './my-cli-logger';

export const customLogger = (namespace: string): Interfaces.Logger => {
  const myLogger = new Logger(namespace);
  return {
    child: (ns: string, delimiter?: string) => customLogger(`${namespace}${delimiter ?? ':'}${ns}`),
    debug: (formatter: unknown, ...args: unknown[]) => myLogger.debug(format(formatter, ...args)),
    error: (formatter: unknown, ...args: unknown[]) => myLogger.error(format(formatter, ...args)),
    info: (formatter: unknown, ...args: unknown[]) => myLogger.info(format(formatter, ...args)),
    trace: (formatter: unknown, ...args: unknown[]) => myLogger.trace(format(formatter, ...args)),
    warn: (formatter: unknown, ...args: unknown[]) => myLogger.warn(format(formatter, ...args)),
    namespace,
  };
};

export const logger = customLogger('my-cli');
```

```javascript
// bin/run.js
#!/usr/bin/env node

async function main() {
  const {execute} = await import('@oclif/core');
  const { logger } = await import('../dist/oclif-logger.js');
  await oclif.execute({
    dir: import.meta.url,
    loadOptions: {
      root: import.meta.dirname,
      logger,
    },
  });
}

await main();
```

You can also provide the logger to `Config`, in the event that you instantiate `Config` before calling `run` or `execute`

```typescript
import {Config, run} from '@oclif/core'
const config = await config.load({
  logger,
});

await run(process.argv.slice(2), config)
```


@W-15470578@


Patch:
diff --git a/package.json b/package.json
index 6cd1aa6..5d77e83 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@oclif/core",
   "description": "base library for oclif CLIs",
-  "version": "4.0.0-beta.2",
+  "version": "4.0.0-beta.3",
   "author": "Salesforce",
   "bugs": "https://github.com/oclif/core/issues",
   "dependencies": {
@@ -17,6 +17,7 @@
     "is-wsl": "^2.2.0",
     "minimatch": "^9.0.4",
     "string-width": "^4.2.3",
+    "supports-color": "^9.4.0",
     "widest-line": "^3.1.0",
     "wordwrap": "^1.0.0",
     "wrap-ansi": "^7.0.0"
diff --git a/src/command.ts b/src/command.ts
index c75281a..f898b17 100644
--- a/src/command.ts
+++ b/src/command.ts
@@ -24,6 +24,7 @@ import {
   ParserOutput,
 } from './interfaces/parser'
 import {Plugin} from './interfaces/plugin'
+import {makeDebug} from './logger'
 import * as Parser from './parser'
 import {aggregateFlags} from './util/aggregate-flags'
 import {toConfiguredId} from './util/ids'
@@ -137,7 +138,7 @@ export abstract class Command {
   ) {
     this.id = this.ctor.id
     try {
-      this.debug = require('debug')(this.id ? `${this.config.bin}:${this.id}` : this.config.bin)
+      this.debug = makeDebug(this.id ? `${this.config.bin}:${this.id}` : this.config.bin)
     } catch {
       this.debug = () => {
         // noop
@@ -214,19 +215,10 @@ export abstract class Command {
     Errors.exit(code)
   }
 
-  protected async finally(_: Error | undefined): Promise<any> {
-    try {
-      const {config} = Errors
-      if (config.errorLogger) await config.errorLogger.flush()
-    } catch (error: any) {
-      console.error(error)
-    }
-  }
+  protected async finally(_: Error | undefined): Promise<any> {}
 
   protected async init(): Promise<any> {
     this.debug('init version: %s argv: %o', this.ctor._base, this.argv)
-    if (this.config.debug) Errors.config.debug = true
-    if (this.config.errlog) Errors.config.errlog = this.config.errlog
     const g: any = global
     g['http-call'] = g['http-call'] || {}
     g['http-call']!.userAgent = this.config.userAgent
diff --git a/src/config/config.ts b/src/config/config.ts
index 0bc59c1..7f8a286 100644
--- a/src/config/config.ts
+++ b/src/config/config.ts
@@ -12,6 +12,7 @@ import {Hook, Hooks, PJSON, Topic} from '../interfaces'
 import {ArchTypes, Config as IConfig, LoadOptions, PlatformTypes, VersionDetails} from '../interfaces/config'
 import {Plugin as IPlugin, Options} from '../interfaces/plugin'
 import {Theme} from '../interfaces/theme'
+import {makeDebug as loggerMakeDebug, setLogger} from '../logger'
 import {loadWithData} from '../module-loader'
 import {OCLIF_MARKER_OWNER, Performance} from '../performance'
 import {settings} from '../settings'
@@ -22,15 +23,22 @@ import ux from '../ux'
 import {parseTheme} from '../ux/theme'
 import PluginLoader from './plugin-loader'
 import {tsPath} from './ts-path'
-import {Debug, collectUsableIds, getCommandIdPermutations} from './util'
+import {collectUsableIds, getCommandIdPermutations, makeDebug} from './util'
 
-// eslint-disable-next-line new-cap
-const debug = Debug()
+const debug = makeDebug()
 
 const _pjson = Cache.getInstance().get('@oclif/core')
 const BASE = `${_pjson.name}@${_pjson.version}`
 const ROOT_ONLY_HOOKS = new Set<keyof Hooks>(['preparse'])
 
+function displayWarnings() {
+  if (process.listenerCount('warning') > 1) return
+  process.on('warning', (warning: any) => {
+    console.error(warning.stack)
+    if (warning.detail) console.error(warning.detail)
+  })
+}
+
 function channelFromVersion(version: string) {
   const m = version.match(/[^-]+(?:-([^.]+))?/)
   return (m && m[1]) || 'stable'
@@ -81,9 +89,7 @@ export class Config implements IConfig {
   public channel!: string
   public configDir!: string
   public dataDir!: string
-  public debug = 0
   public dirname!: string
-  public errlog!: string
   public flexibleTaxonomy!: boolean
   public home!: string
   public isSingleCommandCLI = false
@@ -123,6 +129,7 @@ export class Config implements IConfig {
 
   constructor(public options: Options) {}
   static async load(opts: LoadOptions = module.filename || __dirname): Promise<Config> {
+    setLogger(opts)
     // Handle the case when a file URL string is passed in such as 'import.meta.url'; covert to file path.
     if (typeof opts === 'string' && opts.startsWith('file://')) {
       opts = fileURLToPath(opts)
@@ -285,6 +292,8 @@ export class Config implements IConfig {
   public async load(): Promise<void> {
     settings.performanceEnabled =
       (settings.performanceEnabled === undefined ? this.options.enablePerf : settings.performanceEnabled) ?? false
+    if (settings.debug) displayWarnings()
+    setLogger(this.options)
     const marker = Performance.mark(OCLIF_MARKER_OWNER, 'config.load')
     this.pluginLoader = new PluginLoader({plugins: this.options.plugins, root: this.options.root})
     this.rootPlugin = await this.pluginLoader.loadRoot({pjson: this.options.pjson})
@@ -321,13 +330,11 @@ export class Config implements IConfig {
 
     this.userAgent = `${this.name}/${this.version} ${this.platform}-${this.arch} node-${process.version}`
     this.shell = this._shell()
-    this.debug = this._debug()
 
     this.home = process.env.HOME || (this.windows && this.windowsHome()) || getHomeDir() || tmpdir()
     this.cacheDir = this.scopedEnvVar('CACHE_DIR') || this.macosCacheDir() || this.dir('cache')
     this.configDir = this.scopedEnvVar('CONFIG_DIR') || this.dir('config')
     this.dataDir = this.scopedEnvVar('DATA_DIR') || this.dir('data')
-    this.errlog = join(this.cacheDir, 'error.log')
     this.binPath = this.scopedEnvVar('BINPATH')
 
     this.npmRegistry = this.scopedEnvVar('NPM_REGISTRY') || this.pjson.oclif.npmRegistry
@@ -347,6 +354,7 @@ export class Config implements IConfig {
           this.pjson.oclif.commands?.target),
     )
 
+    this.maybeAdjustDebugSetting()
     await this.loadPluginsAndCommands()
 
     debug('config done')
@@ -503,7 +511,7 @@ export class Config implements IConfig {
 
     const plugins = ROOT_ONLY_HOOKS.has(event) ? [this.rootPlugin] : [...this.plugins.values()]
     const promises = plugins.map(async (p) => {
-      const debug = require('debug')([this.bin, p.name, 'hooks', event].join(':'))
+      const debug = loggerMakeDebug([p.name, 'hooks', event].join(':'))
       const context: Hook.Context = {
         config: this,
         debug,
@@ -631,44 +639,6 @@ export class Config implements IConfig {
     return v === '1' || v === 'true'
   }
 
-  protected warn(err: {detail: string; name: string} | Error | string, scope?: string): void {
-    if (this.warned) return
-
-    if (typeof err === 'string') {
-      process.emitWarning(err)
-      return
-    }
-
-    if (err instanceof Error) {
-      const modifiedErr: any = err
-      modifiedErr.name = `${err.name} Plugin: ${this.name}`
-      modifiedErr.detail = compact([
-        (err as any).detail,
-        `module: ${this._base}`,
-        scope && `task: ${scope}`,
-        `plugin: ${this.name}`,
-        `root: ${this.root}`,
-        'See more details with DEBUG=*',
-      ]).join('\n')
-      process.emitWarning(err)
-      return
-    }
-
-    // err is an object
-    process.emitWarning('Config.warn expected either a string or Error, but instead received an object')
-    err.name = `${err.name} Plugin: ${this.name}`
-    err.detail = compact([
-      err.detail,
-      `module: ${this._base}`,
-      scope && `task: ${scope}`,
-      `plugin: ${this.name}`,
-      `root: ${this.root}`,
-      'See more details with DEBUG=*',
-    ]).join('\n')
-
-    process.emitWarning(JSON.stringify(err))
-  }
-
   protected windowsHome(): string | undefined {
     return this.windowsHomedriveHome() || this.windowsUserprofileHome()
   }
@@ -681,16 +651,6 @@ export class Config implements IConfig {
     return process.env.USERPROFILE
   }
 
-  protected _debug(): number {
-    if (this.scopedEnvVarTrue('DEBUG')) return 1
-    try {
-      const {enabled} = require('debug')(this.bin)
-      if (enabled) return 1
-    } catch {}
-
-    return 0
-  }
-
   protected _shell(): string {
     let shellPath
     const {COMSPEC} = process.env
@@ -932,4 +892,49 @@ export class Config implements IConfig {
 
     marker?.stop()
   }
+
+  private maybeAdjustDebugSetting(): void {
+    if (this.scopedEnvVarTrue('DEBUG')) {
+      settings.debug = true
+      displayWarnings()
+    }
+  }
+
+  private warn(err: {detail: string; name: string} | Error | string, scope?: string): void {
+    if (this.warned) return
+
+    if (typeof err === 'string') {
+      process.emitWarning(err)
+      return
+    }
+
+    if (err instanceof Error) {
+      const modifiedErr: any = err
+      modifiedErr.name = `${err.name} Plugin: ${this.name}`
+      modifiedErr.detail = compact([
+        (err as any).detail,
+        `module: ${this._base}`,
+        scope && `task: ${scope}`,
+        `plugin: ${this.name}`,
+        `root: ${this.root}`,
+        'See more details with DEBUG=*',
+      ]).join('\n')
+      process.emitWarning(err)
+      return
+    }
+
+    // err is an object
+    process.emitWarning('Config.warn expected either a string or Error, but instead received an object')
+    err.name = `${err.name} Plugin: ${this.name}`
+    err.detail = compact([
+      err.detail,
+      `module: ${this._base}`,
+      scope && `task: ${scope}`,
+      `plugin: ${this.name}`,
+      `root: ${this.root}`,
+      'See more details with DEBUG=*',
+    ]).join('\n')
+
+    process.emitWarning(JSON.stringify(err))
+  }
 }
diff --git a/src/config/plugin-loader.ts b/src/config/plugin-loader.ts
index 8ac6fbe..27088dc 100644
--- a/src/config/plugin-loader.ts
+++ b/src/config/plugin-loader.ts
@@ -7,10 +7,9 @@ import {OCLIF_MARKER_OWNER, Performance} from '../performance'
 import {readJson} from '../util/fs'
 import {isProd} from '../util/util'
 import * as Plugin from './plugin'
-import {Debug} from './util'
+import {makeDebug} from './util'
 
-// eslint-disable-next-line new-cap
-const debug = Debug()
+const debug = makeDebug()
 
 type PluginLoaderOptions = {
   plugins?: IPlugin[] | PluginsMap
diff --git a/src/config/plugin.ts b/src/config/plugin.ts
index 7d68a76..d898c6d 100644
--- a/src/config/plugin.ts
+++ b/src/config/plugin.ts
@@ -17,7 +17,7 @@ import {findRoot} from '../util/find-root'
 import {readJson} from '../util/fs'
 import {castArray, compact} from '../util/util'
 import {tsPath} from './ts-path'
-import {Debug, getCommandIdPermutations} from './util'
+import {getCommandIdPermutations, makeDebug} from './util'
 
 const _pjson = Cache.getInstance().get('@oclif/core')
 
@@ -142,8 +142,7 @@ export class Plugin implements IPlugin {
 
   _base = `${_pjson.name}@${_pjson.version}`
 
-  // eslint-disable-next-line new-cap
-  protected _debug = Debug()
+  protected _debug = makeDebug()
 
   private commandCache: CommandCache | undefined
   private commandDiscoveryOpts: CommandDiscovery | undefined
@@ -231,8 +230,8 @@ export class Plugin implements IPlugin {
     this.name = this.pjson.name
     this.alias = this.options.name ?? this.pjson.name
     if (!this.name) throw new CLIError(`no name in package.json (${root})`)
-    // eslint-disable-next-line new-cap
-    this._debug = Debug(this.name)
+
+    this._debug = makeDebug(this.name)
     this.version = this.pjson.version
     if (this.pjson.oclif) {
       this.valid = true
diff --git a/src/config/ts-path.ts b/src/config/ts-path.ts
index b06a388..6d2677d 100644
--- a/src/config/ts-path.ts
+++ b/src/config/ts-path.ts
@@ -9,9 +9,9 @@ import {settings} from '../settings'
 import {existsSync} from '../util/fs'
 import {readTSConfig} from '../util/read-tsconfig'
 import {isProd} from '../util/util'
-import {Debug} from './util'
-// eslint-disable-next-line new-cap
-const debug = Debug('ts-path')
+import {makeDebug} from './util'
+
+const debug = makeDebug('ts-path')
 
 export const TS_CONFIGS: Record<string, TSConfig | undefined> = {}
 const REGISTERED = new Set<string>()
diff --git a/src/config/util.ts b/src/config/util.ts
index 77b33da..5b1b45f 100644
--- a/src/config/util.ts
+++ b/src/config/util.ts
@@ -1,22 +1,7 @@
-const debug = require('debug')
+import {getLogger} from '../logger'
 
-function displayWarnings() {
-  if (process.listenerCount('warning') > 1) return
-  process.on('warning', (warning: any) => {
-    console.error(warning.stack)
-    if (warning.detail) console.error(warning.detail)
-  })
-}
-
-export function Debug(...scope: string[]): (..._: any) => void {
-  if (!debug)
-    return (..._: any[]) => {
-      // noop
-    }
-
-  const d = debug(['config', ...scope].join(':'))
-  if (d.enabled) displayWarnings()
-  return (...args: any[]) => d(...args)
+export function makeDebug(...scope: string[]): (..._: any) => void {
+  return (formatter: unknown, ...args: unknown[]) => getLogger(['config', ...scope].join(':')).debug(formatter, ...args)
 }
 
 // Adapted from https://github.com/angus-c/just/blob/master/packages/array-permutations/index.js
diff --git a/src/errors/config.ts b/src/errors/config.ts
deleted file mode 100644
index 506a411..0000000
--- a/src/errors/config.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import {settings} from '../settings'
-import {Logger} from './logger'
-
-function displayWarnings() {
-  if (process.listenerCount('warning') > 1) return
-  process.on('warning', (warning: any) => {
-    console.error(warning.stack)
-    if (warning.detail) console.error(warning.detail)
-  })
-}
-
-export const config = {
-  get debug(): boolean {
-    return Boolean(settings.debug)
-  },
-  set debug(enabled: boolean) {
-    settings.debug = enabled
-    if (enabled) displayWarnings()
-  },
-  get errlog(): string | undefined {
-    return settings.errlog
-  },
-  set errlog(errlog: string | undefined) {
-    if (errlog) {
-      this.errorLogger = new Logger(errlog)
-      settings.errlog = errlog
-    } else {
-      delete this.errorLogger
-      delete settings.errlog
-    }
-  },
-  errorLogger: undefined as Logger | undefined,
-}
diff --git a/src/errors/error.ts b/src/errors/error.ts
index c85e3a3..baa3824 100644
--- a/src/errors/error.ts
+++ b/src/errors/error.ts
@@ -1,6 +1,6 @@
 import {OclifError, PrettyPrintableError} from '../interfaces'
+import {getLogger} from '../logger'
 import {stderr} from '../ux/write'
-import {config} from './config'
 import {CLIError, addOclifExitCode} from './errors/cli'
 import prettyPrint, {applyPrettyPrintOptions} from './errors/pretty-print'
 
@@ -22,7 +22,7 @@ export function error(input: Error | string, options: {exit?: false | number} &
   if (options.exit === false) {
     const message = prettyPrint(err)
     if (message) stderr(message)
-    if (config.errorLogger) config.errorLogger.log(err?.stack ?? '')
+    if (err?.stack) getLogger().error(err.stack)
   } else throw err
 }
 
diff --git a/src/errors/errors/cli.ts b/src/errors/errors/cli.ts
index cacd5df..bda115d 100644
--- a/src/errors/errors/cli.ts
+++ b/src/errors/errors/cli.ts
@@ -6,7 +6,7 @@ import wrap from 'wrap-ansi'
 import Cache from '../../cache'
 import {OclifError, PrettyPrintableError} from '../../interfaces/errors'
 import {errtermwidth} from '../../screen'
-import {config} from '../config'
+import {settings} from '../../settings'
 
 /**
  * properties specific to internal oclif error handling
@@ -49,7 +49,7 @@ export class CLIError extends Error implements OclifError {
    * @return {string} returns a string representing the dispay of the error
    */
   render(): string {
-    if (config.debug) {
+    if (settings.debug) {
       return this.stack
     }
 
diff --git a/src/errors/errors/pretty-print.ts b/src/errors/errors/pretty-print.ts
index 46712f5..b908e3d 100644
--- a/src/errors/errors/pretty-print.ts
+++ b/src/errors/errors/pretty-print.ts
@@ -3,7 +3,7 @@ import wrap from 'wrap-ansi'
 
 import {PrettyPrintableError} from '../../interfaces/errors'
 import {errtermwidth} from '../../screen'
-import {config} from '../config'
+import {settings} from '../../settings'
 
 // These exist for backwards compatibility with CLIError
 type CLIErrorDisplayOptions = {bang?: string; name?: string}
@@ -31,7 +31,7 @@ const formatSuggestions = (suggestions?: string[]): string | undefined => {
 }
 
 export default function prettyPrint(error: Error & PrettyPrintableError & CLIErrorDisplayOptions): string | undefined {
-  if (config.debug) {
+  if (settings.debug) {
     return error.stack
   }
 
diff --git a/src/errors/handle.ts b/src/errors/handle.ts
index ee81a63..90443dc 100644
--- a/src/errors/handle.ts
+++ b/src/errors/handle.ts
@@ -3,8 +3,8 @@ import clean from 'clean-stack'
 import Cache from '../cache'
 import {Help} from '../help/index'
 import {OclifError, PrettyPrintableError} from '../interfaces'
+import {getLogger} from '../logger'
 import {CLIParseError} from '../parser/errors'
-import {config} from './config'
 import {CLIError} from './errors/cli'
 import {ExitError} from './errors/exit'
 import prettyPrint from './errors/pretty-print'
@@ -53,16 +53,11 @@ export async function handle(err: ErrorToHandle): Promise<void> {
 
     const exitCode = err.oclif?.exit ?? 1
 
-    if (config.errorLogger && err.code !== 'EEXIT') {
-      if (stack) {
-        config.errorLogger.log(stack)
-      }
+    if (err.code !== 'EEXIT' && stack) {
+      getLogger().error(stack)
+    }
 
-      await config.errorLogger
-        .flush()
-        .then(() => Exit.exit(exitCode))
-        .catch(console.error)
-    } else Exit.exit(exitCode)
+    Exit.exit(exitCode)
   } catch (error: any) {
     console.error(err.stack)
     console.error(error.stack)
diff --git a/src/errors/index.ts b/src/errors/index.ts
index 612318b..70d8836 100644
--- a/src/errors/index.ts
+++ b/src/errors/index.ts
@@ -1,11 +1,8 @@
 export {PrettyPrintableError} from '../interfaces'
-export {config} from './config'
 export {error} from './error'
 export {CLIError} from './errors/cli'
 export {ExitError} from './errors/exit'
 export {ModuleLoadError} from './errors/module-load'
 export {exit} from './exit'
 export {handle} from './handle'
-export {Logger} from './logger'
-
 export {warn} from './warn'
diff --git a/src/errors/logger.ts b/src/errors/logger.ts
deleted file mode 100644
index 2791a90..0000000
--- a/src/errors/logger.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import ansis from 'ansis'
-import {appendFile, mkdir} from 'node:fs/promises'
-import {dirname} from 'node:path'
-
-const timestamp = () => new Date().toISOString()
-let timer: any
-const wait = (ms: number) =>
-  new Promise((resolve) => {
-    if (timer) timer.unref()
-    timer = setTimeout(() => resolve(null), ms)
-  })
-
-function chomp(s: string): string {
-  if (s.endsWith('\n')) return s.replace(/\n$/, '')
-  return s
-}
-
-export class Logger {
-  protected buffer: string[] = []
-
-  protected flushing: Promise<void> = Promise.resolve()
-
-  constructor(public file: string) {}
-
-  async flush(waitForMs = 0): Promise<void> {
-    await wait(waitForMs)
-    this.flushing = this.flushing.then(async () => {
-      if (this.buffer.length === 0) return
-      const mylines = this.buffer
-      this.buffer = []
-      await mkdir(dirname(this.file), {recursive: true})
-      await appendFile(this.file, mylines.join('\n') + '\n')
-    })
-    await this.flushing
-  }
-
-  log(msg: string): void {
-    msg = ansis.strip(chomp(msg))
-    const lines = msg.split('\n').map((l) => `${timestamp()} ${l}`.trimEnd())
-    this.buffer.push(...lines)
-    this.flush(50).catch(console.error)
-  }
-}
diff --git a/src/errors/warn.ts b/src/errors/warn.ts
index dd7f4c6..9d2c857 100644
--- a/src/errors/warn.ts
+++ b/src/errors/warn.ts
@@ -1,6 +1,6 @@
 import {OclifError} from '../interfaces'
+import {getLogger} from '../logger'
 import {stderr} from '../ux/write'
-import {config} from './config'
 import {CLIError, addOclifExitCode} from './errors/cli'
 import prettyPrint from './errors/pretty-print'
 
@@ -36,7 +36,7 @@ export function warn(input: Error | string, options?: Options): void {
 
   const message = prettyPrint(err)
   if (message) stderr(message)
-  if (config.errorLogger) config.errorLogger.log(err?.stack ?? '')
+  if (err?.stack) getLogger().error(err.stack)
 }
 
 export default warn
diff --git a/src/index.ts b/src/index.ts
index 1b7ac58..13cbcd1 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -22,6 +22,7 @@ export {CommandHelp, Help, HelpBase, loadHelpClass} from './help'
 export {HelpSection, HelpSectionKeyValueTable, HelpSectionRenderer} from './help/formatter'
 export * as Interfaces from './interfaces'
 export {Hook} from './interfaces/hooks'
+export {getLogger} from './logger'
 export {run} from './main'
 export * as ModuleLoader from './module-loader'
 export * as Parser from './parser'
diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts
index bbe6939..930305f 100644
--- a/src/interfaces/config.ts
+++ b/src/interfaces/config.ts
@@ -60,22 +60,10 @@ export interface Config {
    * example: ~/.local/share/mycli
    */
   readonly dataDir: string
-  /**
-   * debugging level
-   *
-   * set by ${BIN}_DEBUG or DEBUG=$BIN
-   */
-  readonly debug: number
   /**
    * base dirname to use in cacheDir/configDir/dataDir
    */
   readonly dirname: string
-  /**
-   * points to a file that should be appended to for error logs
-   *
-   * example: ~/Library/Caches/mycli/error.log
-   */
-  readonly errlog: string
   findCommand(id: string, opts: {must: true}): Command.Loadable
   findCommand(id: string, opts?: {must: boolean}): Command.Loadable | undefined
   findMatches(id: string, argv: string[]): Command.Loadable[]
diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts
index e6ba59c..e033e95 100644
--- a/src/interfaces/index.ts
+++ b/src/interfaces/index.ts
@@ -5,6 +5,7 @@ export type {CommandError, OclifError, PrettyPrintableError} from './errors'
 export type {InferredFlags} from './flags'
 export type {HelpOptions} from './help'
 export type {Hook, Hooks} from './hooks'
+export type {Logger} from './logger'
 export type {Manifest} from './manifest'
 export type {Arg, BooleanFlag, CustomOptions, Deprecation, Flag, FlagDefinition, OptionFlag} from './parser'
 export type {PJSON} from './pjson'
diff --git a/src/interfaces/logger.ts b/src/interfaces/logger.ts
new file mode 100644
index 0000000..5f4836c
--- /dev/null
+++ b/src/interfaces/logger.ts
@@ -0,0 +1,9 @@
+export type Logger = {
+  debug: (formatter: unknown, ...args: unknown[]) => void
+  error: (formatter: unknown, ...args: unknown[]) => void
+  info: (formatter: unknown, ...args: unknown[]) => void
+  trace: (formatter: unknown, ...args: unknown[]) => void
+  warn: (formatter: unknown, ...args: unknown[]) => void
+  child: (namespace: string) => Logger
+  namespace: string
+}
diff --git a/src/interfaces/plugin.ts b/src/interfaces/plugin.ts
index b4b3403..6d23ae7 100644
--- a/src/interfaces/plugin.ts
+++ b/src/interfaces/plugin.ts
@@ -1,4 +1,5 @@
 import {Command} from '../command'
+import {Logger} from './logger'
 import {HookOptions, PJSON} from './pjson'
 import {Topic} from './topic'
 
@@ -23,6 +24,7 @@ export interface Options extends PluginOptions {
   devPlugins?: boolean
   enablePerf?: boolean
   jitPlugins?: boolean
+  logger?: Logger
   pjson?: PJSON.Plugin
   pluginAdditions?: {
     core?: string[]
diff --git a/src/logger.ts b/src/logger.ts
new file mode 100644
index 0000000..e663f57
--- /dev/null
+++ b/src/logger.ts
@@ -0,0 +1,94 @@
+import makeDebugger from 'debug'
+
+import {LoadOptions} from './interfaces/config'
+import {Logger} from './interfaces/logger'
+
+const OCLIF_NS = 'oclif'
+
+function makeLogger(namespace: string = OCLIF_NS): Logger {
+  const debug = makeDebugger(namespace)
+  return {
+    child: (ns: string, delimiter?: string) => makeLogger(`${namespace}${delimiter ?? ':'}${ns}`),
+    debug,
+    error: (formatter: unknown, ...args: unknown[]) => makeLogger(`${namespace}:error`).debug(formatter, ...args),
+    info: debug,
+    namespace,
+    trace: debug,
+    warn: debug,
+  }
+}
+
+/**
+ * Cache of logger instances. This is used to prevent creating multiple logger instances for the same namespace.
+ *
+ * The root logger is stored under the 'root' key as well as it's namespace.
+ */
+const cachedLoggers = new Map<string, Logger>()
+
+/**
+ * Returns a logger instance for the given namespace.
+ * If a namespace is provided, a child logger is returned.
+ * If no namespace is provided, the root logger is returned.
+ */
+export function getLogger(namespace?: string): Logger {
+  let rootLogger = cachedLoggers.get('root')
+  if (!rootLogger) {
+    set(makeLogger(OCLIF_NS))
+  }
+
+  rootLogger = cachedLoggers.get('root')!
+
+  if (namespace) {
+    const cachedLogger = cachedLoggers.get(namespace)
+    if (cachedLogger) return cachedLogger
+
+    const logger = rootLogger.child(namespace)
+    cachedLoggers.set(namespace, logger)
+    return logger
+  }
+
+  return rootLogger
+}
+
+function ensureItMatchesInterface(newLogger: Logger): boolean {
+  return (
+    typeof newLogger.child === 'function' &&
+    typeof newLogger.debug === 'function' &&
+    typeof newLogger.error === 'function' &&
+    typeof newLogger.info === 'function' &&
+    typeof newLogger.trace === 'function' &&
+    typeof newLogger.warn === 'function' &&
+    typeof newLogger.namespace === 'string'
+  )
+}
+
+function set(newLogger: Logger): void {
+  if (cachedLoggers.has(newLogger.namespace)) return
+  if (cachedLoggers.has('root')) return
+
+  if (ensureItMatchesInterface(newLogger)) {
+    cachedLoggers.set(newLogger.namespace, newLogger)
+    cachedLoggers.set('root', newLogger)
+  } else {
+    process.emitWarning('Logger does not match the Logger interface. Using default logger.')
+  }
+}
+
+/**
+ * Convenience function to create a debug function for a specific namespace
+ */
+export function makeDebug(namespace: string): Logger['debug'] {
+  return (formatter: unknown, ...args: unknown[]) => getLogger(namespace).debug(formatter, ...args)
+}
+
+export function setLogger(loadOptions: LoadOptions) {
+  if (loadOptions && typeof loadOptions !== 'string' && 'logger' in loadOptions && loadOptions.logger) {
+    set(loadOptions.logger)
+  } else {
+    set(makeLogger(OCLIF_NS))
+  }
+}
+
+export function clearLoggers() {
+  cachedLoggers.clear()
+}
diff --git a/src/main.ts b/src/main.ts
index 02f946e..9b41b15 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -4,12 +4,11 @@ import Cache from './cache'
 import {Config} from './config'
 import {getHelpFlagAdditions, loadHelpClass, normalizeArgv} from './help'
 import * as Interfaces from './interfaces'
+import {getLogger, setLogger} from './logger'
 import {OCLIF_MARKER_OWNER, Performance} from './performance'
 import {SINGLE_COMMAND_CLI_SYMBOL} from './symbols'
 import ux from './ux'
 
-const debug = require('debug')('oclif:main')
-
 export const helpAddition = (argv: string[], config: Interfaces.Config): boolean => {
   if (argv.length === 0 && !config.isSingleCommandCLI) return true
   const mergedHelpFlags = getHelpFlagAdditions(config)
@@ -46,6 +45,9 @@ export async function run(argv?: string[], options?: Interfaces.LoadOptions): Pr
     await help.showHelp(argv)
   }
 
+  setLogger(options)
+
+  const {debug} = getLogger('main')
   debug(`process.execPath: ${process.execPath}`)
   debug(`process.execArgv: ${process.execArgv}`)
   debug('process.argv: %O', process.argv)
diff --git a/src/parser/parse.ts b/src/parser/parse.ts
index 037b652..72d7902 100644
--- a/src/parser/parse.ts
+++ b/src/parser/parse.ts
@@ -19,6 +19,7 @@ import {
   ParserOutput,
   ParsingToken,
 } from '../interfaces/parser'
+import {makeDebug} from '../logger'
 import {isTruthy, last, pickBy} from '../util/util'
 import {ArgInvalidOptionError, CLIError, FlagInvalidOptionError} from './errors'
 
@@ -26,7 +27,7 @@ let debug: any
 try {
   debug =
     process.env.CLI_FLAGS_DEBUG === '1'
-      ? require('debug')('../parser')
+      ? makeDebug('parser')
       : () => {
           // noop
         }
diff --git a/src/performance.ts b/src/performance.ts
index 24a3359..16f9feb 100644
--- a/src/performance.ts
+++ b/src/performance.ts
@@ -1,5 +1,6 @@
 import {PerformanceObserver, performance} from 'node:perf_hooks'
 
+import {makeDebug} from './logger'
 import {settings} from './settings'
 
 type Details = Record<string, boolean | number | string | string[]>
@@ -193,7 +194,7 @@ export class Performance {
   public static debug(): void {
     if (!Performance.enabled) return
 
-    const oclifDebug = require('debug')('oclif-perf')
+    const oclifDebug = makeDebug('perf')
     const processUpTime = (process.uptime() * 1000).toFixed(4)
     oclifDebug('Process Uptime: %sms', processUpTime)
     oclifDebug('Oclif Time: %sms', Performance.oclifPerf['oclif.runMs'].toFixed(4))
@@ -239,7 +240,7 @@ export class Performance {
       )
     }
 
-    const nonCoreDebug = require('debug')('non-oclif-perf')
+    const nonCoreDebug = makeDebug('non-oclif-perf')
 
     const nonCorePerf = Performance.results
     if (nonCorePerf.size > 0) {
diff --git a/src/settings.ts b/src/settings.ts
index 6ee729c..2e98bc2 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -13,12 +13,6 @@ export type Settings = {
    * oclif.settings.debug = true;
    */
   debug?: boolean
-  /**
-   * The path to the error.log file.
-   *
-   * NOTE: This is read-only and setting it will have no effect.
-   */
-  errlog?: string
   /**
    * Enable performance tracking. Resulting data is available in the `perf` property of the `Config` class.
    * This will be overridden by the `enablePerf` property passed into Config constructor.
diff --git a/src/util/find-root.ts b/src/util/find-root.ts
index 303df74..6ce3df1 100644
--- a/src/util/find-root.ts
+++ b/src/util/find-root.ts
@@ -1,13 +1,16 @@
 /* eslint-disable no-await-in-loop */
 import type {PackageInformation, PackageLocator, getPackageInformation} from 'pnpapi'
 
-import makeDebug from 'debug'
 import {basename, dirname, join} from 'node:path'
 
 import {PJSON} from '../interfaces'
+import {getLogger} from '../logger'
 import {safeReadJson} from './fs'
 
-const debug = makeDebug('find-root')
+export function debug(...scope: string[]): (..._: any) => void {
+  return (formatter: unknown, ...args: unknown[]) =>
+    getLogger(['find-root', ...scope].join(':')).debug(formatter, ...args)
+}
 
 // essentially just "cd .."
 function* up(from: string) {
@@ -28,11 +31,11 @@ function* up(from: string) {
 async function findPluginRoot(root: string, name?: string) {
   // If we know the plugin name then we just need to traverse the file
   // system until we find the directory that matches the plugin name.
-  debug.extend(name ?? 'root-plugin')(`Finding root starting at ${root}`)
+  debug(name ?? 'root-plugin')(`Finding root starting at ${root}`)
   if (name) {
     for (const next of up(root)) {
       if (next.endsWith(basename(name))) {
-        debug.extend(name)('Found root based on plugin name!')
+        debug(name)('Found root based on plugin name!')
         return next
       }
     }
@@ -51,9 +54,9 @@ async function findPluginRoot(root: string, name?: string) {
 
     try {
       const cur = join(next, 'package.json')
-      debug.extend(name ?? 'root-plugin')(`Checking ${cur}`)
+      debug(name ?? 'root-plugin')(`Checking ${cur}`)
       if (await safeReadJson<PJSON>(cur)) {
-        debug.extend(name ?? 'root-plugin')('Found root by traversing up from starting point!')
+        debug(name ?? 'root-plugin')('Found root by traversing up from starting point!')
         return dirname(cur)
       }
     } catch {}
@@ -67,7 +70,7 @@ async function findPluginRoot(root: string, name?: string) {
  * See https://github.com/oclif/config/pull/289#issuecomment-983904051
  */
 async function findRootLegacy(name: string | undefined, root: string): Promise<string | undefined> {
-  debug.extend(name ?? 'root-plugin')('Finding root using legacy method')
+  debug(name ?? 'root-plugin')('Finding root using legacy method')
   for (const next of up(root)) {
     let cur
     if (name) {
@@ -120,7 +123,7 @@ function findPnpRoot(name: string, root: string): string | undefined {
   maybeRequirePnpApi(root)
   if (!pnp) return
 
-  debug.extend(name)('Finding root for using pnp method')
+  debug(name)('Finding root for using pnp method')
   const seen = new Set()
 
   const traverseDependencyTree = (locator: PackageLocator, parentPkg?: PackageInformation): string | undefined => {
@@ -177,30 +180,30 @@ function findPnpRoot(name: string, root: string): string | undefined {
  */
 export async function findRoot(name: string | undefined, root: string) {
   if (name) {
-    debug.extend(name)(`Finding root using ${root}`)
+    debug(name)(`Finding root using ${root}`)
     let pkgPath
     try {
       pkgPath = require.resolve(name, {paths: [root]})
-      debug.extend(name)(`Found starting point with require.resolve`)
+      debug(name)(`Found starting point with require.resolve`)
     } catch {
-      debug.extend(name)(`require.resolve could not find plugin starting point`)
+      debug(name)(`require.resolve could not find plugin starting point`)
     }
 
     if (pkgPath) {
       const found = await findPluginRoot(dirname(pkgPath), name)
       if (found) {
-        debug.extend(name)(`Found root at ${found}`)
+        debug(name)(`Found root at ${found}`)
         return found
       }
     }
 
     const found = process.versions.pnp ? findPnpRoot(name, root) : await findRootLegacy(name, root)
-    debug.extend(name)(found ? `Found root at ${found}` : 'No root found!')
+    debug(name)(found ? `Found root at ${found}` : 'No root found!')
     return found
   }
 
-  debug.extend('root-plugin')(`Finding root plugin using ${root}`)
+  debug('root-plugin')(`Finding root plugin using ${root}`)
   const found = await findPluginRoot(root)
-  debug.extend('root-plugin')(found ? `Found root at ${found}` : 'No root found!')
+  debug('root-plugin')(found ? `Found root at ${found}` : 'No root found!')
   return found
 }
diff --git a/src/util/read-tsconfig.ts b/src/util/read-tsconfig.ts
index 9ffd86f..6e9df69 100644
--- a/src/util/read-tsconfig.ts
+++ b/src/util/read-tsconfig.ts
@@ -1,9 +1,9 @@
-import makeDebug from 'debug'
 import {readFile, readdir} from 'node:fs/promises'
 import {dirname, join} from 'node:path'
 
 import {warn} from '../errors/warn'
 import {TSConfig} from '../interfaces'
+import {makeDebug} from '../logger'
 import {mergeNestedObjects} from './util'
 
 const debug = makeDebug('read-tsconfig')


Test patch:
diff --git a/test/config/config.test.ts b/test/config/config.test.ts
index 9c07fe0..e3ec9e8 100644
--- a/test/config/config.test.ts
+++ b/test/config/config.test.ts
@@ -122,7 +122,6 @@ describe('Config', () => {
 
       expect(config).to.have.property('cacheDir', join('/my/home/Library/Caches/@oclif/core'))
       expect(config).to.have.property('configDir', join('/my/home/.config/@oclif/core'))
-      expect(config).to.have.property('errlog', join('/my/home/Library/Caches/@oclif/core/error.log'))
       expect(config).to.have.property('dataDir', join('/my/home/.local/share/@oclif/core'))
       expect(config).to.have.property('home', join('/my/home'))
     })
@@ -136,7 +135,6 @@ describe('Config', () => {
 
       expect(config).to.have.property('cacheDir', join('/my/home/.cache/@oclif/core'))
       expect(config).to.have.property('configDir', join('/my/home/.config/@oclif/core'))
-      expect(config).to.have.property('errlog', join('/my/home/.cache/@oclif/core/error.log'))
       expect(config).to.have.property('dataDir', join('/my/home/.local/share/@oclif/core'))
       expect(config).to.have.property('home', join('/my/home'))
     })
@@ -151,7 +149,6 @@ describe('Config', () => {
 
       expect(config).to.have.property('cacheDir', join('/my/home/localappdata/@oclif\\core'))
       expect(config).to.have.property('configDir', join('/my/home/localappdata/@oclif\\core'))
-      expect(config).to.have.property('errlog', join('/my/home/localappdata/@oclif\\core/error.log'))
       expect(config).to.have.property('dataDir', join('/my/home/localappdata/@oclif\\core'))
       expect(config).to.have.property('home', join('/my/home'))
     })
diff --git a/test/errors/handle.test.ts b/test/errors/handle.test.ts
index 7ec273b..c9fd5a7 100644
--- a/test/errors/handle.test.ts
+++ b/test/errors/handle.test.ts
@@ -1,14 +1,11 @@
 import {expect} from 'chai'
-import {readFileSync} from 'node:fs'
-import {join} from 'node:path'
-import * as process from 'node:process'
+import process from 'node:process'
 import {SinonSandbox, SinonStub, createSandbox} from 'sinon'
 
-import {CLIError, ExitError, config, exit as exitErrorThrower} from '../../src/errors'
+import {CLIError, ExitError, exit as exitErrorThrower} from '../../src/errors'
 import {Exit, handle} from '../../src/errors/handle'
 import {captureOutput} from '../test'
 
-const errlog = join(__dirname, '../tmp/mytest/error.log')
 const x = process.platform === 'win32' ? '»' : '›'
 
 describe('handle', () => {
@@ -49,22 +46,9 @@ describe('handle', () => {
     expect(stderr).to.be.empty
   })
 
-  describe('errlog', () => {
-    beforeEach(() => {
-      config.errlog = errlog
-    })
-
-    afterEach(() => {
-      config.errlog = undefined
-    })
-
-    it('logs when errlog is set', async () => {
-      const {stderr} = await captureOutput(() => handle(new CLIError('uh oh!')))
-      expect(stderr).to.equal(` ${x}   Error: uh oh!\n`)
-      await config.errorLogger!.flush()
-      expect(readFileSync(errlog, 'utf8')).to.contain('Error: uh oh!')
-      expect(exitStub.firstCall.firstArg).to.equal(2)
-    })
+  it('logs error with symbol', async () => {
+    const {stderr} = await captureOutput(() => handle(new CLIError('uh oh!')))
+    expect(stderr).to.equal(` ${x}   Error: uh oh!\n`)
   })
 
   it('should use default exit code for Error (1)', async () => {
diff --git a/test/errors/pretty-print.test.ts b/test/errors/pretty-print.test.ts
index c75a203..1355b4b 100644
--- a/test/errors/pretty-print.test.ts
+++ b/test/errors/pretty-print.test.ts
@@ -1,8 +1,8 @@
 import ansis from 'ansis'
 import {expect} from 'chai'
 
+import {settings} from '../../src'
 import {CLIError} from '../../src/errors'
-import {config} from '../../src/errors/config'
 import prettyPrint from '../../src/errors/errors/pretty-print'
 import {PrettyPrintableError} from '../../src/interfaces/errors'
 
@@ -58,16 +58,16 @@ describe('pretty-print', () => {
     })
   })
 
-  describe('config.debug set to true', () => {
-    let initialConfigDebug: any
+  describe('settings.debug set to true', () => {
+    let initialSettingsDebug: boolean | undefined
 
     beforeEach(() => {
-      initialConfigDebug = config.debug
-      config.debug = true
+      initialSettingsDebug = settings.debug
+      settings.debug = true
     })
 
     afterEach(() => {
-      config.debug = initialConfigDebug
+      settings.debug = initialSettingsDebug
     })
 
     it('shows the stack for an error', async () => {
diff --git a/test/errors/warn.test.ts b/test/errors/warn.test.ts
index 4c0a50b..a9c97ef 100644
--- a/test/errors/warn.test.ts
+++ b/test/errors/warn.test.ts
@@ -1,25 +1,11 @@
 import {expect} from 'chai'
-import {readFile} from 'node:fs/promises'
-import {join} from 'node:path'
 
-import {config, warn} from '../../src/errors'
+import {warn} from '../../src/errors'
 import {captureOutput} from '../test'
 
-const errlog = join(__dirname, '../tmp/mytest/warn.log')
-
 describe('warn', () => {
-  beforeEach(() => {
-    config.errlog = errlog
-  })
-
-  afterEach(() => {
-    config.errlog = undefined
-  })
-
   it('warns', async () => {
     const {stderr} = await captureOutput(async () => warn('foo!'))
     expect(stderr).to.contain('Warning: foo!')
-    await config.errorLogger!.flush()
-    expect(await readFile(errlog, 'utf8')).to.contain('Warning: foo!')
   })
 })
diff --git a/test/logger.test.ts b/test/logger.test.ts
new file mode 100644
index 0000000..c47e753
--- /dev/null
+++ b/test/logger.test.ts
@@ -0,0 +1,115 @@
+import {expect} from 'chai'
+import sinon from 'sinon'
+
+import {Config} from '../src/config/config'
+import {clearLoggers, getLogger, setLogger} from '../src/logger'
+
+describe('getLogger', () => {
+  let sandbox: sinon.SinonSandbox
+  let childStub: sinon.SinonStub
+  let config: Config
+
+  beforeEach(async () => {
+    clearLoggers()
+    sandbox = sinon.createSandbox()
+    childStub = sandbox
+      .stub()
+      .callsFake((ns: string, delimiter?: string) => customLogger(`MY_CLI${delimiter ?? ':'}${ns}`))
+    const customLogger = (namespace: string) => ({
+      child: childStub,
+      debug(_formatter: unknown, ..._args: unknown[]) {},
+      error(_formatter: unknown, ..._args: unknown[]) {},
+      info(_formatter: unknown, ..._args: unknown[]) {},
+      trace(_formatter: unknown, ..._args: unknown[]) {},
+      warn(_formatter: unknown, ..._args: unknown[]) {},
+      namespace,
+    })
+
+    config = await Config.load({
+      root: __dirname,
+      logger: customLogger('MY_CLI'),
+    })
+    setLogger(config)
+  })
+
+  afterEach(() => {
+    sandbox.restore()
+  })
+
+  it('should return the root logger if no namespace is provided', () => {
+    const logger = getLogger()
+    expect(logger.namespace).to.equal('MY_CLI')
+  })
+
+  it('should create a new logger if the namespace has not been created', () => {
+    const logger = getLogger('test')
+    expect(logger.namespace).to.equal('MY_CLI:test')
+    expect(childStub.withArgs('test').callCount).to.equal(1)
+  })
+
+  it('should return a cached logger if the namespace has already been created', () => {
+    getLogger('test')
+    expect(childStub.withArgs('test').callCount, 'first getLogger call should call .child()').to.equal(1)
+
+    const logger = getLogger('test')
+
+    expect(logger.namespace).to.equal('MY_CLI:test')
+    expect(childStub.withArgs('test').callCount, 'second getLogger call should not call .child()').to.equal(1)
+  })
+
+  it('should return default oclif logger if no custom logger is set', async () => {
+    clearLoggers()
+    const logger = getLogger()
+    expect(logger.namespace).to.equal('oclif')
+  })
+})
+
+describe('setLogger', () => {
+  const customLogger = (namespace: string) => ({
+    child: (ns: string, delimiter?: string) => customLogger(`${namespace}${delimiter ?? ':'}${ns}`),
+    debug(_formatter: unknown, ..._args: unknown[]) {},
+    error(_formatter: unknown, ..._args: unknown[]) {},
+    info(_formatter: unknown, ..._args: unknown[]) {},
+    trace(_formatter: unknown, ..._args: unknown[]) {},
+    warn(_formatter: unknown, ..._args: unknown[]) {},
+    namespace,
+  })
+
+  beforeEach(async () => {
+    clearLoggers()
+  })
+
+  it('should set the logger to the custom logger provided in loadOptions', async () => {
+    const config = await Config.load({
+      root: __dirname,
+      logger: customLogger('MY_CLI'),
+    })
+    setLogger(config)
+
+    const logger = getLogger()
+    expect(logger.namespace).to.equal('MY_CLI')
+  })
+
+  it('should set the logger to the default oclif logger if no custom logger is provided', async () => {
+    const config = await Config.load({
+      root: __dirname,
+    })
+    setLogger(config)
+
+    const logger = getLogger()
+    expect(logger.namespace).to.equal('oclif')
+  })
+
+  it('should use default logger if the provided logger does not match the Logger interface', async () => {
+    const logger = customLogger('MY_CLI')
+    // @ts-expect-error because we are testing an invalid logger interface
+    delete logger.child
+    const config = await Config.load({
+      root: __dirname,
+      logger,
+    })
+    setLogger(config)
+    const oclifLogger = getLogger()
+    expect(oclifLogger.namespace).to.equal('oclif')
+  })
+})