diff --git a/.eslintrc.json b/.eslintrc.json
index f6bea009..d64ecb6e 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -11,6 +11,7 @@
     "@typescript-eslint/semi": "warn",
     "curly": "warn",
     "eqeqeq": ["warn", "always", { "null": "ignore" }],
+    "no-console": "warn",
     "no-throw-literal": "warn",
     "semi": "off"
   },
diff --git a/package.json b/package.json
index 8d018326..1da65875 100644
--- a/package.json
+++ b/package.json
@@ -61,6 +61,10 @@
       {
         "command": "vscode-deephaven.selectConnection",
         "title": "Deephaven: Select Connection"
+      },
+      {
+        "command": "vscode-deephaven.downloadLogs",
+        "title": "Deephaven: Download Logs"
       }
     ],
     "menus": {
diff --git a/src/common/constants.ts b/src/common/constants.ts
index 407d5a6c..25468874 100644
--- a/src/common/constants.ts
+++ b/src/common/constants.ts
@@ -2,6 +2,7 @@ export const CONFIG_KEY = 'vscode-deephaven';
 export const CONFIG_CORE_SERVERS = 'core-servers';
 
 // export const DHFS_SCHEME = 'dhfs';
+export const DOWNLOAD_LOGS_CMD = `${CONFIG_KEY}.downloadLogs`;
 export const RUN_CODE_COMMAND = `${CONFIG_KEY}.runCode`;
 export const RUN_SELECTION_COMMAND = `${CONFIG_KEY}.runSelection`;
 export const SELECT_CONNECTION_COMMAND = `${CONFIG_KEY}.selectConnection`;
@@ -9,3 +10,5 @@ export const SELECT_CONNECTION_COMMAND = `${CONFIG_KEY}.selectConnection`;
 export const STATUS_BAR_DISCONNECTED_TEXT = 'Deephaven: Disconnected';
 export const STATUS_BAR_DISCONNECT_TEXT = 'Deephaven: Disconnect';
 export const STATUS_BAR_CONNECTING_TEXT = 'Deephaven: Connecting...';
+
+export const DOWNLOAD_LOGS_TEXT = 'Download Logs';
diff --git a/src/extension.ts b/src/extension.ts
index 22f5e8a4..9bc38a5c 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -7,25 +7,41 @@ import {
   createConnectionOptions,
   createConnectionQuickPick,
   getTempDir,
+  Logger,
+  Toaster,
 } from './util';
 import { DhcService } from './services';
 import { DhServiceRegistry } from './services';
 import {
+  DOWNLOAD_LOGS_CMD,
   RUN_CODE_COMMAND,
   RUN_SELECTION_COMMAND,
   SELECT_CONNECTION_COMMAND,
 } from './common';
+import { OutputChannelWithHistory } from './util/OutputChannelWithHistory';
 
-export function activate(context: vscode.ExtensionContext) {
-  console.log(
-    'Congratulations, your extension "vscode-deephaven" is now active!'
-  );
+const logger = new Logger('extension');
 
+export function activate(context: vscode.ExtensionContext) {
   let selectedConnectionUrl: string | null = null;
   let selectedDhService: DhcService | null = null;
   let connectionOptions = createConnectionOptions();
 
   const outputChannel = vscode.window.createOutputChannel('Deephaven', 'log');
+  const debugOutputChannel = new OutputChannelWithHistory(
+    context,
+    vscode.window.createOutputChannel('Deephaven Debug', 'log')
+  );
+  const toaster = new Toaster();
+
+  // Configure log handlers
+  Logger.addConsoleHandler();
+  Logger.addOutputChannelHandler(debugOutputChannel);
+
+  logger.info(
+    'Congratulations, your extension "vscode-deephaven" is now active!'
+  );
+
   outputChannel.appendLine('Deephaven extension activated');
 
   // Update connection options when configuration changes
@@ -41,13 +57,12 @@ export function activate(context: vscode.ExtensionContext) {
   const dhcServiceRegistry = new DhServiceRegistry(
     DhcService,
     new ExtendedMap<string, vscode.WebviewPanel>(),
-    outputChannel
+    outputChannel,
+    toaster
   );
 
   dhcServiceRegistry.addEventListener('disconnect', serverUrl => {
-    vscode.window.showInformationMessage(
-      `Disconnected from Deephaven server: ${serverUrl}`
-    );
+    toaster.info(`Disconnected from Deephaven server: ${serverUrl}`);
     clearConnection();
   });
 
@@ -79,15 +94,19 @@ export function activate(context: vscode.ExtensionContext) {
   }
 
   /** Register extension commands */
-  const { runCodeCmd, runSelectionCmd, selectConnectionCmd } = registerCommands(
-    () => connectionOptions,
-    getActiveDhService,
-    onConnectionSelected
-  );
+  const { downloadLogsCmd, runCodeCmd, runSelectionCmd, selectConnectionCmd } =
+    registerCommands(
+      () => connectionOptions,
+      getActiveDhService,
+      onConnectionSelected,
+      onDownloadLogs
+    );
 
   const connectStatusBarItem = createConnectStatusBarItem();
 
   context.subscriptions.push(
+    debugOutputChannel,
+    downloadLogsCmd,
     dhcServiceRegistry,
     outputChannel,
     runCodeCmd,
@@ -99,6 +118,18 @@ export function activate(context: vscode.ExtensionContext) {
   // recreate tmp dir that will be used to dowload JS Apis
   getTempDir(true /*recreate*/);
 
+  /**
+   * Handle download logs command
+   */
+  async function onDownloadLogs() {
+    const uri = await debugOutputChannel.downloadHistoryToFile();
+
+    if (uri != null) {
+      toaster.info(`Downloaded logs to ${uri.fsPath}`);
+      vscode.window.showTextDocument(uri);
+    }
+  }
+
   /**
    * Handle connection selection
    */
@@ -163,8 +194,14 @@ async function ensureUriEditorIsActive(uri: vscode.Uri) {
 function registerCommands(
   getConnectionOptions: () => ConnectionOption[],
   getActiveDhService: (autoActivate: boolean) => Promise<DhcService | null>,
-  onConnectionSelected: (connectionUrl: string | null) => void
+  onConnectionSelected: (connectionUrl: string | null) => void,
+  onDownloadLogs: () => void
 ) {
+  const downloadLogsCmd = vscode.commands.registerCommand(
+    DOWNLOAD_LOGS_CMD,
+    onDownloadLogs
+  );
+
   /** Run all code in active editor */
   const runCodeCmd = vscode.commands.registerCommand(
     RUN_CODE_COMMAND,
@@ -213,5 +250,5 @@ function registerCommands(
     }
   );
 
-  return { runCodeCmd, runSelectionCmd, selectConnectionCmd };
+  return { downloadLogsCmd, runCodeCmd, runSelectionCmd, selectConnectionCmd };
 }
diff --git a/src/services/CacheService.ts b/src/services/CacheService.ts
index 47de24e6..0f3bf5c0 100644
--- a/src/services/CacheService.ts
+++ b/src/services/CacheService.ts
@@ -1,7 +1,9 @@
 import { Disposable } from '../common';
-import { isDisposable } from '../util';
+import { isDisposable, Logger } from '../util';
 import { EventDispatcher } from './EventDispatcher';
 
+const logger = new Logger('CacheService');
+
 export class CacheService<T, TEventName extends string>
   extends EventDispatcher<TEventName>
   implements Disposable
@@ -27,7 +29,7 @@ export class CacheService<T, TEventName extends string>
     const normalizeKey = this.normalizeKey(key);
 
     if (!this.cachedPromises.has(normalizeKey)) {
-      console.log(`${this.label}: caching key: ${normalizeKey}`);
+      logger.info(`${this.label}: caching key: ${normalizeKey}`);
       // Note that we cache the promise itself, not the result of the promise.
       // This helps ensure the loader is only called the first time `get` is
       // called.
@@ -49,7 +51,7 @@ export class CacheService<T, TEventName extends string>
         }
       });
     } catch (err) {
-      console.error('An error occurred while disposing cached values:', err);
+      logger.error('An error occurred while disposing cached values:', err);
     }
 
     this.cachedPromises.clear();
diff --git a/src/services/DhService.ts b/src/services/DhService.ts
index faee8637..05048cf7 100644
--- a/src/services/DhService.ts
+++ b/src/services/DhService.ts
@@ -2,9 +2,11 @@ import * as vscode from 'vscode';
 import type { dh as DhcType } from '../dh/dhc-types';
 import { hasErrorCode } from '../util/typeUtils';
 import { ConnectionAndSession, Disposable } from '../common';
-import { ExtendedMap, formatTimestamp } from '../util';
+import { ExtendedMap, formatTimestamp, Logger, Toaster } from '../util';
 import { EventDispatcher } from './EventDispatcher';
 
+const logger = new Logger('DhService');
+
 /* eslint-disable @typescript-eslint/naming-convention */
 const icons = {
   Figure: '📈',
@@ -33,19 +35,22 @@ export abstract class DhService<TDH, TClient>
   constructor(
     serverUrl: string,
     panelRegistry: ExtendedMap<string, vscode.WebviewPanel>,
-    outputChannel: vscode.OutputChannel
+    outputChannel: vscode.OutputChannel,
+    toaster: Toaster
   ) {
     super();
 
     this.serverUrl = serverUrl;
     this.panelRegistry = panelRegistry;
     this.outputChannel = outputChannel;
+    this.toaster = toaster;
   }
 
   public readonly serverUrl: string;
   protected readonly subscriptions: (() => void)[] = [];
 
   protected outputChannel: vscode.OutputChannel;
+  protected toaster: Toaster;
   private panelRegistry: ExtendedMap<string, vscode.WebviewPanel>;
   private cachedCreateClient: Promise<TClient> | null = null;
   private cachedCreateSession: Promise<ConnectionAndSession<
@@ -118,11 +123,11 @@ export abstract class DhService<TDH, TClient>
       );
     } catch (err) {
       this.clearCaches();
-      console.error(err);
+      logger.error(err);
       this.outputChannel.appendLine(
         `Failed to initialize Deephaven API${err == null ? '.' : `: ${err}`}`
       );
-      vscode.window.showErrorMessage('Failed to initialize Deephaven API');
+      this.toaster.error('Failed to initialize Deephaven API');
       return false;
     }
 
@@ -172,15 +177,13 @@ export abstract class DhService<TDH, TClient>
     if (this.cn == null || this.session == null) {
       this.clearCaches();
 
-      vscode.window.showErrorMessage(
+      this.toaster.error(
         `Failed to create Deephaven session: ${this.serverUrl}`
       );
 
       return false;
     } else {
-      vscode.window.showInformationMessage(
-        `Created Deephaven session: ${this.serverUrl}`
-      );
+      this.toaster.info(`Created Deephaven session: ${this.serverUrl}`);
 
       return true;
     }
@@ -192,7 +195,7 @@ export abstract class DhService<TDH, TClient>
   ): Promise<void> {
     if (editor.document.languageId !== 'python') {
       // This should not actually happen
-      console.log(`languageId '${editor.document.languageId}' not supported.`);
+      logger.info(`languageId '${editor.document.languageId}' not supported.`);
       return;
     }
 
@@ -220,7 +223,7 @@ export abstract class DhService<TDH, TClient>
 
     const text = editor.document.getText(selectionRange);
 
-    console.log('Sending text to dh:', text);
+    logger.info('Sending text to dh:', text);
 
     let result: CommandResultBase;
     let error: string | null = null;
@@ -235,7 +238,7 @@ export abstract class DhService<TDH, TClient>
       // clear the caches on connection disconnect
       if (hasErrorCode(err, 16)) {
         this.clearCaches();
-        vscode.window.showErrorMessage(
+        this.toaster.error(
           'Session is no longer invalid. Please re-run the command to reconnect.'
         );
         return;
@@ -243,12 +246,10 @@ export abstract class DhService<TDH, TClient>
     }
 
     if (error) {
-      console.error(error);
+      logger.error(error);
       this.outputChannel.show(true);
       this.outputChannel.appendLine(error);
-      vscode.window.showErrorMessage(
-        'An error occurred when running a command'
-      );
+      this.toaster.error('An error occurred when running a command');
 
       return;
     }
diff --git a/src/services/DhServiceRegistry.ts b/src/services/DhServiceRegistry.ts
index 7b902a5d..a8326d41 100644
--- a/src/services/DhServiceRegistry.ts
+++ b/src/services/DhServiceRegistry.ts
@@ -1,7 +1,7 @@
 import * as vscode from 'vscode';
 import { CacheService } from './CacheService';
 import { DhcService, DhcServiceConstructor } from './DhcService';
-import { ensureHasTrailingSlash, ExtendedMap } from '../util';
+import { ensureHasTrailingSlash, ExtendedMap, Toaster } from '../util';
 
 export class DhServiceRegistry<T extends DhcService> extends CacheService<
   T,
@@ -10,7 +10,8 @@ export class DhServiceRegistry<T extends DhcService> extends CacheService<
   constructor(
     serviceFactory: DhcServiceConstructor<T>,
     panelRegistry: ExtendedMap<string, vscode.WebviewPanel>,
-    outputChannel: vscode.OutputChannel
+    outputChannel: vscode.OutputChannel,
+    toaster: Toaster
   ) {
     super(
       serviceFactory.name,
@@ -22,7 +23,8 @@ export class DhServiceRegistry<T extends DhcService> extends CacheService<
         const dhService = new serviceFactory(
           serverUrl,
           panelRegistry,
-          outputChannel
+          outputChannel,
+          toaster
         );
 
         // Propagate service events as registry events.
diff --git a/src/services/DhcService.ts b/src/services/DhcService.ts
index 4cd686da..7d5f2d1d 100644
--- a/src/services/DhcService.ts
+++ b/src/services/DhcService.ts
@@ -8,13 +8,16 @@ import {
   initDhcApi,
   initDhcSession,
 } from '../dh/dhc';
-import { ExtendedMap, getPanelHtml } from '../util';
+import { ExtendedMap, getPanelHtml, Logger, Toaster } from '../util';
 import { ConnectionAndSession } from '../common';
 
+const logger = new Logger('DhcService');
+
 export type DhcServiceConstructor<T extends DhcService> = new (
   serverUrl: string,
   panelRegistry: ExtendedMap<string, vscode.WebviewPanel>,
-  outputChannel: vscode.OutputChannel
+  outputChannel: vscode.OutputChannel,
+  toaster: Toaster
 ) => T;
 
 export class DhcService extends DhService<typeof DhcType, DhcType.CoreClient> {
@@ -30,7 +33,7 @@ export class DhcService extends DhService<typeof DhcType, DhcType.CoreClient> {
     try {
       return new dh.CoreClient(this.serverUrl);
     } catch (err) {
-      console.error(err);
+      logger.error(err);
       throw err;
     }
   }
@@ -68,7 +71,7 @@ export class DhcService extends DhService<typeof DhcType, DhcType.CoreClient> {
         this.psk = token;
       }
     } catch (err) {
-      console.error(err);
+      logger.error(err);
     }
 
     return connectionAndSession;
diff --git a/src/services/PanelFocusManager.ts b/src/services/PanelFocusManager.ts
index 64365b51..ebddc649 100644
--- a/src/services/PanelFocusManager.ts
+++ b/src/services/PanelFocusManager.ts
@@ -1,4 +1,7 @@
 import * as vscode from 'vscode';
+import { Logger } from '../util';
+
+const logger = new Logger('PanelFocusManager');
 
 /*
  * Panels steal focus when they finish loading which causes the run
@@ -26,7 +29,7 @@ export class PanelFocusManager {
   >();
 
   initialize(panel: vscode.WebviewPanel): void {
-    console.log('Initializing panel:', panel.title, 2);
+    logger.info('Initializing panel:', panel.title, 2);
 
     // Only count the last panel initialized
     this.panelsPendingInitialFocus = new WeakMap();
@@ -59,7 +62,7 @@ export class PanelFocusManager {
 
       const pendingChangeCount = this.panelsPendingInitialFocus.get(panel) ?? 0;
 
-      console.log('Panel view state changed:', {
+      logger.info('Panel view state changed:', {
         panelTitle: panel.title,
         activeEditorViewColumn,
         activeTabGroupViewColumn,
diff --git a/src/util/Logger.ts b/src/util/Logger.ts
new file mode 100644
index 00000000..85c3784d
--- /dev/null
+++ b/src/util/Logger.ts
@@ -0,0 +1,91 @@
+import * as vscode from 'vscode';
+
+export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'debug2';
+
+export type LogLevelHandler = (label: string, ...args: unknown[]) => void;
+
+export type LogHandler = Record<LogLevel, LogLevelHandler>;
+
+/**
+ * Simple logger delegate that can be used to log messages to a set of handlers.
+ * Messages will include a label for scoping log messages.
+ *
+ * Handlers can be statically registered via:
+ *
+ *   Logger.handlers.add(handler);
+ *
+ * Then modules can create labeled loggers and use them to log messages:
+ *
+ *   const logger = new Logger('MyModule');
+ *   logger.info('Hello, world!');
+ */
+export class Logger {
+  static handlers: Set<LogHandler> = new Set();
+
+  /**
+   * Register log handler that logs to console.
+   */
+  static addConsoleHandler = () => {
+    Logger.handlers.add({
+      /* eslint-disable no-console */
+      error: console.error.bind(console),
+      warn: console.warn.bind(console),
+      info: console.info.bind(console),
+      debug: console.debug.bind(console),
+      debug2: console.debug.bind(console),
+      /* eslint-enable no-console */
+    });
+  };
+
+  /**
+   * Register a log handler that logs to a `vscode.OutputChannel`.
+   * @param outputChannel
+   */
+  static addOutputChannelHandler = (outputChannel: vscode.OutputChannel) => {
+    Logger.handlers.add({
+      error: (label, ...args) =>
+        outputChannel.appendLine(`${label} ERROR: ${args.join(', ')}`),
+      warn: (label, ...args) =>
+        outputChannel.appendLine(`${label} WARN: ${args.join(', ')}`),
+      info: (label, ...args) =>
+        outputChannel.appendLine(`${label} INFO: ${args.join(', ')}`),
+      debug: (label, ...args) =>
+        outputChannel.appendLine(`${label} DEBUG: ${args.join(', ')}`),
+      debug2: (label, ...args) =>
+        outputChannel.appendLine(`${label} DEBUG2: ${args.join(', ')}`),
+    });
+  };
+
+  constructor(private readonly label: string) {}
+
+  /**
+   * Handle log args for a given level
+   * @param level The level to handle
+   * @param args The arguments to log
+   */
+  private handle = (level: LogLevel, ...args: unknown[]) => {
+    Logger.handlers.forEach(handler =>
+      handler[level](`[${this.label}]`, ...args)
+    );
+  };
+
+  debug = (...args: unknown[]): void => {
+    this.handle('debug', ...args);
+  };
+
+  debug2 = (...args: unknown[]): void => {
+    this.handle('debug2', ...args);
+  };
+
+  info = (...args: unknown[]): void => {
+    this.handle('info', ...args);
+  };
+
+  warn = (...args: unknown[]): void => {
+    this.handle('warn', ...args);
+  };
+
+  error = (...args: unknown[]): void => {
+    this.handle('error', ...args);
+  };
+}
diff --git a/src/util/OutputChannelWithHistory.ts b/src/util/OutputChannelWithHistory.ts
new file mode 100644
index 00000000..16676445
--- /dev/null
+++ b/src/util/OutputChannelWithHistory.ts
@@ -0,0 +1,129 @@
+import * as vscode from 'vscode';
+import * as fs from 'node:fs/promises';
+
+/**
+ * Output channel wrapper that keeps a history of all appended lines.
+ */
+export class OutputChannelWithHistory implements vscode.OutputChannel {
+  constructor(
+    readonly context: vscode.ExtensionContext,
+    readonly outputChannel: vscode.OutputChannel
+  ) {
+    this.name = outputChannel.name;
+
+    // Have to bind this explicitly since function overloads prevent using
+    // lexical binding via arrow function.
+    this.show = this.show.bind(this);
+  }
+
+  private history: string[] = [];
+  readonly name: string;
+
+  /**
+   * Append the given value to the channel. Also appends to the history.
+   *
+   * @param value A string, falsy values will not be printed.
+   */
+  append = (value: string): void => {
+    this.history.push(value);
+    return this.outputChannel.append(value);
+  };
+
+  /**
+   * Append the given value and a line feed character
+   * to the channel. Also appends to the history.
+   *
+   * @param value A string, falsy values will be printed.
+   */
+  appendLine = (value: string) => {
+    this.history.push(value);
+    this.outputChannel.appendLine(value);
+  };
+
+  /**
+   * Clear the history.
+   */
+  clearHistory = () => {
+    this.history = [];
+  };
+
+  downloadHistoryToFile = async (): Promise<vscode.Uri | null> => {
+    const response = await vscode.window.showSaveDialog({
+      defaultUri: vscode.Uri.file(
+        `deephaven-vscode_${new Date()
+          .toISOString()
+          .substring(0, 19)
+          .replace(/[:]/g, '')
+          .replace('T', '_')}.log`
+      ),
+      filters: {
+        // eslint-disable-next-line @typescript-eslint/naming-convention
+        Logs: ['log'],
+      },
+    });
+
+    if (response?.fsPath == null) {
+      return null;
+    }
+
+    await fs.writeFile(response.fsPath, this.history.join('\n'));
+
+    return response;
+  };
+
+  /**
+   * Dispose and free associated resources.
+   */
+  dispose = (): void => {
+    this.clearHistory();
+    this.outputChannel.dispose();
+  };
+
+  /**
+   * Removes all output from the channel. Also clears the history.
+   */
+  clear = (): void => {
+    this.clearHistory();
+    this.outputChannel.clear();
+  };
+
+  /**
+   * Hide this channel from the UI.
+   */
+  hide = (): void => {
+    return this.outputChannel.hide();
+  };
+
+  /**
+   * Replaces all output from the channel with the given value. Also replaces
+   * the history.
+   *
+   * @param value A string, falsy values will not be printed.
+   */
+  replace = (value: string): void => {
+    this.history = [value];
+    this.outputChannel.replace(value);
+  };
+
+  /**
+   * Reveal this channel in the UI.
+   *
+   * @param preserveFocus When `true` the channel will not take focus.
+   */
+  show(preserveFocus?: boolean): void;
+  /**
+   * Reveal this channel in the UI.
+   *
+   * @deprecated Use the overload with just one parameter (`show(preserveFocus?: boolean): void`).
+   *
+   * @param column This argument is **deprecated** and will be ignored.
+   * @param preserveFocus When `true` the channel will not take focus.
+   */
+  show(column?: vscode.ViewColumn, preserveFocus?: boolean): void;
+  show(column?: unknown, preserveFocus?: unknown): void {
+    return this.outputChannel.show(
+      column as Parameters<vscode.OutputChannel['show']>[0],
+      preserveFocus as Parameters<vscode.OutputChannel['show']>[1]
+    );
+  }
+}
diff --git a/src/util/Toaster.ts b/src/util/Toaster.ts
new file mode 100644
index 00000000..2eb0c50b
--- /dev/null
+++ b/src/util/Toaster.ts
@@ -0,0 +1,25 @@
+import * as vscode from 'vscode';
+import { DOWNLOAD_LOGS_CMD, DOWNLOAD_LOGS_TEXT } from '../common';
+
+/**
+ * Show toast messages to user.
+ */
+export class Toaster {
+  constructor() {}
+
+  error = async (message: string) => {
+    const response = await vscode.window.showErrorMessage(
+      message,
+      DOWNLOAD_LOGS_TEXT
+    );
+
+    // If user clicks "Download Logs" button
+    if (response === DOWNLOAD_LOGS_TEXT) {
+      await vscode.commands.executeCommand(DOWNLOAD_LOGS_CMD);
+    }
+  };
+
+  info = async (message: string) => {
+    await vscode.window.showInformationMessage(message);
+  };
+}
diff --git a/src/util/downloadUtils.ts b/src/util/downloadUtils.ts
index 5612b60d..ff3e320e 100644
--- a/src/util/downloadUtils.ts
+++ b/src/util/downloadUtils.ts
@@ -2,6 +2,9 @@ import * as fs from 'node:fs';
 import * as http from 'node:http';
 import * as https from 'node:https';
 import * as path from 'node:path';
+import { Logger } from './Logger';
+
+const logger = new Logger('downloadUtils');
 
 export function getTempDir(recreate = false) {
   const tempDir = path.join(__dirname, 'tmp');
@@ -60,12 +63,12 @@ export async function downloadFromURL(
         });
       })
       .on('timeout', () => {
-        console.error('Failed download of url:', url);
+        logger.error('Failed download of url:', url);
         reject();
       })
       .on('error', e => {
         if (retries > 0) {
-          console.error('Retrying url:', url);
+          logger.error('Retrying url:', url);
           setTimeout(
             () =>
               downloadFromURL(url, retries - 1, retryDelay).then(
@@ -75,10 +78,10 @@ export async function downloadFromURL(
             retryDelay
           );
         } else {
-          console.error(
+          logger.error(
             `Hit retry limit. Stopping attempted include from ${url} with error`
           );
-          console.error(e);
+          logger.error(e);
           reject();
         }
       });
diff --git a/src/util/index.ts b/src/util/index.ts
index eac63594..cee0db9b 100644
--- a/src/util/index.ts
+++ b/src/util/index.ts
@@ -1,7 +1,9 @@
 export * from './downloadUtils';
 export * from './ExtendedMap';
 export * from './isDisposable';
+export * from './Logger';
 export * from './panelUtils';
 export * from './polyfillUtils';
+export * from './Toaster';
 export * from './uiUtils';
 export * from './urlUtils';