diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 070ed52521..88eb4077bb 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -1101,7 +1101,6 @@
         "confirmOnDelete": "Confirm On Delete",
         "developer": "Developer",
         "displayInProgress": "Display Progress Images",
-        "enableImageDebugging": "Enable Image Debugging",
         "enableInformationalPopovers": "Enable Informational Popovers",
         "informationalPopoversDisabled": "Informational Popovers Disabled",
         "informationalPopoversDisabledDesc": "Informational popovers have been disabled. Enable them in Settings.",
@@ -1812,5 +1811,30 @@
             "upscaling": "Upscaling",
             "upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)"
         }
+    },
+    "system": {
+        "enableLogging": "Enable Logging",
+        "logLevel": {
+            "logLevel": "Log Level",
+            "trace": "Trace",
+            "debug": "Debug",
+            "info": "Info",
+            "warn": "Warn",
+            "error": "Error",
+            "fatal": "Fatal"
+        },
+        "logNamespaces": {
+            "logNamespaces": "Log Namespaces",
+            "gallery": "Gallery",
+            "models": "Models",
+            "config": "Config",
+            "canvas": "Canvas",
+            "generation": "Generation",
+            "workflows": "Workflows",
+            "system": "System",
+            "events": "Events",
+            "queue": "Queue",
+            "metadata": "Metadata"
+        }
     }
 }
diff --git a/invokeai/frontend/web/src/app/logging/logger.ts b/invokeai/frontend/web/src/app/logging/logger.ts
index 3d35681dc4..d8450db8bb 100644
--- a/invokeai/frontend/web/src/app/logging/logger.ts
+++ b/invokeai/frontend/web/src/app/logging/logger.ts
@@ -15,24 +15,23 @@ export const BASE_CONTEXT = {};
 
 export const $logger = atom<Logger>(Roarr.child(BASE_CONTEXT));
 
-export type LoggerNamespace =
-  | 'images'
-  | 'models'
-  | 'config'
-  | 'canvas'
-  | 'generation'
-  | 'nodes'
-  | 'system'
-  | 'socketio'
-  | 'session'
-  | 'queue'
-  | 'dnd'
-  | 'controlLayers'
-  | 'metadata'
-  | 'konva'
-  | 'worker';
+export const zLogNamespace = z.enum([
+  'gallery',
+  'models',
+  'config',
+  'canvas',
+  'generation',
+  'workflows',
+  'system',
+  'events',
+  'queue',
+  'metadata',
+]);
+const zLogNamespacesArray = z.array(zLogNamespace);
+export type LogNamespace = z.infer<typeof zLogNamespace>;
+export const isLogNamespaceArray = (v: unknown): v is LogNamespace[] => zLogNamespacesArray.safeParse(v).success;
 
-export const logger = (namespace: LoggerNamespace) => $logger.get().child({ namespace });
+export const logger = (namespace: LogNamespace) => $logger.get().child({ namespace });
 
 export const zLogLevel = z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']);
 export type LogLevel = z.infer<typeof zLogLevel>;
diff --git a/invokeai/frontend/web/src/app/logging/useLogger.ts b/invokeai/frontend/web/src/app/logging/useLogger.ts
index 6e170ca376..f292f1f929 100644
--- a/invokeai/frontend/web/src/app/logging/useLogger.ts
+++ b/invokeai/frontend/web/src/app/logging/useLogger.ts
@@ -3,27 +3,35 @@ import { useAppSelector } from 'app/store/storeHooks';
 import { useEffect, useMemo } from 'react';
 import { ROARR, Roarr } from 'roarr';
 
-import type { LoggerNamespace } from './logger';
+import type { LogNamespace } from './logger';
 import { $logger, BASE_CONTEXT, LOG_LEVEL_MAP, logger } from './logger';
 
-export const useLogger = (namespace: LoggerNamespace) => {
-  const consoleLogLevel = useAppSelector((s) => s.system.consoleLogLevel);
-  const shouldLogToConsole = useAppSelector((s) => s.system.shouldLogToConsole);
+export const useLogger = (namespace: LogNamespace) => {
+  const logLevel = useAppSelector((s) => s.system.logLevel);
+  const logNamespaces = useAppSelector((s) => s.system.logNamespaces);
+  const logIsEnabled = useAppSelector((s) => s.system.logIsEnabled);
 
   // The provided Roarr browser log writer uses localStorage to config logging to console
   useEffect(() => {
-    if (shouldLogToConsole) {
+    if (logIsEnabled) {
       // Enable console log output
       localStorage.setItem('ROARR_LOG', 'true');
 
       // Use a filter to show only logs of the given level
-      localStorage.setItem('ROARR_FILTER', `context.logLevel:>=${LOG_LEVEL_MAP[consoleLogLevel]}`);
+      let filter = `context.logLevel:>=${LOG_LEVEL_MAP[logLevel]}`;
+      if (logNamespaces.length > 0) {
+        filter += ' AND ';
+        filter = `(${logNamespaces.map((ns) => `context.namespace:${ns}`).join(' OR ')})`;
+      } else {
+        filter += ' AND context.namespace:undefined';
+      }
+      localStorage.setItem('ROARR_FILTER', filter);
     } else {
       // Disable console log output
       localStorage.setItem('ROARR_LOG', 'false');
     }
     ROARR.write = createLogWriter();
-  }, [consoleLogLevel, shouldLogToConsole]);
+  }, [logLevel, logIsEnabled, logNamespaces]);
 
   // Update the module-scoped logger context as needed
   useEffect(() => {
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener.ts
index 65f3198b91..8358a1192d 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener.ts
@@ -8,14 +8,14 @@ import { t } from 'i18next';
 import { queueApi } from 'services/api/endpoints/queue';
 import type { BatchConfig, ImageDTO } from 'services/api/types';
 
+const log = logger('queue');
+
 export const adHocPostProcessingRequested = createAction<{ imageDTO: ImageDTO }>(`upscaling/postProcessingRequested`);
 
 export const addAdHocPostProcessingRequestedListener = (startAppListening: AppStartListening) => {
   startAppListening({
     actionCreator: adHocPostProcessingRequested,
     effect: async (action, { dispatch, getState }) => {
-      const log = logger('session');
-
       const { imageDTO } = action.payload;
       const state = getState();
 
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts
index 6b8d9782ca..8363c0ff66 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener.ts
@@ -13,12 +13,12 @@ import { queueApi } from 'services/api/endpoints/queue';
 import { $lastCanvasProgressEvent } from 'services/events/setEventListeners';
 import { assert } from 'tsafe';
 
+const log = logger('canvas');
+
 export const addStagingListeners = (startAppListening: AppStartListening) => {
   startAppListening({
     actionCreator: sessionStagingAreaReset,
     effect: async (_, { dispatch }) => {
-      const log = logger('canvas');
-
       try {
         const req = dispatch(
           queueApi.endpoints.cancelByBatchOrigin.initiate(
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/batchEnqueued.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/batchEnqueued.ts
index 3f74bf9b61..0b5e181bcb 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/batchEnqueued.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/batchEnqueued.ts
@@ -7,6 +7,8 @@ import { t } from 'i18next';
 import { truncate, upperFirst } from 'lodash-es';
 import { queueApi } from 'services/api/endpoints/queue';
 
+const log = logger('queue');
+
 export const addBatchEnqueuedListener = (startAppListening: AppStartListening) => {
   // success
   startAppListening({
@@ -14,7 +16,7 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) =
     effect: async (action) => {
       const response = action.payload;
       const arg = action.meta.arg.originalArgs;
-      logger('queue').debug({ enqueueResult: parseify(response) }, 'Batch enqueued');
+      log.debug({ enqueueResult: parseify(response) }, 'Batch enqueued');
 
       toast({
         id: 'QUEUE_BATCH_SUCCEEDED',
@@ -42,7 +44,7 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) =
           status: 'error',
           description: t('common.unknownError'),
         });
-        logger('queue').error({ batchConfig: parseify(arg), error: parseify(response) }, t('queue.batchFailedToQueue'));
+        log.error({ batchConfig: parseify(arg), error: parseify(response) }, t('queue.batchFailedToQueue'));
         return;
       }
 
@@ -68,7 +70,7 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) =
           description: t('common.unknownError'),
         });
       }
-      logger('queue').error({ batchConfig: parseify(arg), error: parseify(response) }, t('queue.batchFailedToQueue'));
+      log.error({ batchConfig: parseify(arg), error: parseify(response) }, t('queue.batchFailedToQueue'));
     },
   });
 };
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/bulkDownload.tsx b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/bulkDownload.tsx
index 049b28ff84..738bd3af13 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/bulkDownload.tsx
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/bulkDownload.tsx
@@ -4,7 +4,7 @@ import { toast } from 'features/toast/toast';
 import { t } from 'i18next';
 import { imagesApi } from 'services/api/endpoints/images';
 
-const log = logger('images');
+const log = logger('gallery');
 
 export const addBulkDownloadListeners = (startAppListening: AppStartListening) => {
   startAppListening({
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts
index eccf8b5889..6f7f6a9e9a 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet.ts
@@ -7,11 +7,12 @@ import { toast } from 'features/toast/toast';
 import { t } from 'i18next';
 import { imagesApi } from 'services/api/endpoints/images';
 
+const log = logger('canvas');
+
 export const addCanvasImageToControlNetListener = (startAppListening: AppStartListening) => {
   startAppListening({
     actionCreator: canvasImageToControlAdapter,
     effect: async (action, { dispatch, getState }) => {
-      const log = logger('canvas');
       const state = getState();
       const { id } = action.payload;
 
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts
index 707820bda3..5e5193f290 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor.ts
@@ -25,7 +25,7 @@ import { assert } from 'tsafe';
 const matcher = isAnyOf(caImageChanged, caProcessedImageChanged, caProcessorConfigChanged, caModelChanged, caRecalled);
 
 const DEBOUNCE_MS = 300;
-const log = logger('session');
+const log = logger('queue');
 
 /**
  * Simple helper to cancel a batch and reset the pending batch ID
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts
index ac32d6fa01..ad16650bf7 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas.ts
@@ -6,7 +6,7 @@ import { parseify } from 'common/util/serialize';
 import { canvasBatchIdAdded, stagingAreaInitialized } from 'features/canvas/store/canvasSlice';
 import { getCanvasData } from 'features/canvas/util/getCanvasData';
 import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode';
-import { blobToDataURL } from "features/controlLayers/konva/util";
+import { blobToDataURL } from 'features/controlLayers/konva/util';
 import { canvasGraphBuilt } from 'features/nodes/store/actions';
 import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
 import { buildCanvasGraph } from 'features/nodes/util/graph/canvas/buildCanvasGraph';
@@ -14,6 +14,8 @@ import { imagesApi } from 'services/api/endpoints/images';
 import { queueApi } from 'services/api/endpoints/queue';
 import type { ImageDTO } from 'services/api/types';
 
+const log = logger('queue');
+
 /**
  * This listener is responsible invoking the canvas. This involves a number of steps:
  *
@@ -32,7 +34,6 @@ export const addEnqueueRequestedCanvasListener = (startAppListening: AppStartLis
     predicate: (action): action is ReturnType<typeof enqueueRequested> =>
       enqueueRequested.match(action) && action.payload.tabName === 'canvas',
     effect: async (action, { getState, dispatch }) => {
-      const log = logger('queue');
       const { prepend } = action.payload;
       const state = getState();
 
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema.ts
index 923b2c0197..4622ff2d8c 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema.ts
@@ -6,11 +6,12 @@ import { parseSchema } from 'features/nodes/util/schema/parseSchema';
 import { size } from 'lodash-es';
 import { appInfoApi } from 'services/api/endpoints/appInfo';
 
+const log = logger('system');
+
 export const addGetOpenAPISchemaListener = (startAppListening: AppStartListening) => {
   startAppListening({
     matcher: appInfoApi.endpoints.getOpenAPISchema.matchFulfilled,
     effect: (action, { getState }) => {
-      const log = logger('system');
       const schemaJSON = action.payload;
 
       log.debug({ schemaJSON: parseify(schemaJSON) }, 'Received OpenAPI schema');
@@ -30,7 +31,6 @@ export const addGetOpenAPISchemaListener = (startAppListening: AppStartListening
       // If action.meta.condition === true, the request was canceled/skipped because another request was in flight or
       // the value was already in the cache. We don't want to log these errors.
       if (!action.meta.condition) {
-        const log = logger('system');
         log.error({ error: parseify(action.error) }, 'Problem retrieving OpenAPI Schema');
       }
     },
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts
index 5412e0f236..38e2127c0d 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts
@@ -2,15 +2,13 @@ import { logger } from 'app/logging/logger';
 import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
 import { imagesApi } from 'services/api/endpoints/images';
 
+const log = logger('gallery');
+
 export const addImageAddedToBoardFulfilledListener = (startAppListening: AppStartListening) => {
   startAppListening({
     matcher: imagesApi.endpoints.addImageToBoard.matchFulfilled,
     effect: (action) => {
-      const log = logger('images');
       const { board_id, imageDTO } = action.meta.arg.originalArgs;
-
-      // TODO: update listImages cache for this board
-
       log.debug({ board_id, imageDTO }, 'Image added to board');
     },
   });
@@ -18,9 +16,7 @@ export const addImageAddedToBoardFulfilledListener = (startAppListening: AppStar
   startAppListening({
     matcher: imagesApi.endpoints.addImageToBoard.matchRejected,
     effect: (action) => {
-      const log = logger('images');
       const { board_id, imageDTO } = action.meta.arg.originalArgs;
-
       log.debug({ board_id, imageDTO }, 'Problem adding image to board');
     },
   });
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts
index 2459f2db4f..858a8cea0b 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners.ts
@@ -13,6 +13,8 @@ import { forEach, intersectionBy } from 'lodash-es';
 import { imagesApi } from 'services/api/endpoints/images';
 import type { ImageDTO } from 'services/api/types';
 
+const log = logger('gallery');
+
 // Some utils to delete images from different parts of the app
 const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
   state.nodes.present.nodes.forEach((node) => {
@@ -185,7 +187,6 @@ export const addImageDeletionListeners = (startAppListening: AppStartListening)
   startAppListening({
     matcher: imagesApi.endpoints.deleteImage.matchFulfilled,
     effect: (action) => {
-      const log = logger('images');
       log.debug({ imageDTO: action.meta.arg.originalArgs }, 'Image deleted');
     },
   });
@@ -193,7 +194,6 @@ export const addImageDeletionListeners = (startAppListening: AppStartListening)
   startAppListening({
     matcher: imagesApi.endpoints.deleteImage.matchRejected,
     effect: (action) => {
-      const log = logger('images');
       log.debug({ imageDTO: action.meta.arg.originalArgs }, 'Unable to delete image');
     },
   });
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts
index 9adb0da5bc..6a7302a55a 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts
@@ -2,11 +2,7 @@ import { createAction } from '@reduxjs/toolkit';
 import { logger } from 'app/logging/logger';
 import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
 import { parseify } from 'common/util/serialize';
-import {
-  ipaImageChanged,
-  rasterLayerAdded,
-  rgIPAdapterImageChanged,
-} from 'features/controlLayers/store/canvasV2Slice';
+import { ipaImageChanged, rasterLayerAdded, rgIPAdapterImageChanged } from 'features/controlLayers/store/canvasV2Slice';
 import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
 import { imageDTOToImageObject } from 'features/controlLayers/store/types';
 import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
@@ -26,11 +22,12 @@ export const dndDropped = createAction<{
   activeData: TypesafeDraggableData;
 }>('dnd/dndDropped');
 
+const log = logger('system');
+
 export const addImageDroppedListener = (startAppListening: AppStartListening) => {
   startAppListening({
     actionCreator: dndDropped,
     effect: (action, { dispatch, getState }) => {
-      const log = logger('dnd');
       const { activeData, overData } = action.payload;
       if (!isValidDrop(overData, activeData)) {
         return;
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard.ts
index 274e4c51c2..c25f521609 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard.ts
@@ -2,13 +2,13 @@ import { logger } from 'app/logging/logger';
 import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
 import { imagesApi } from 'services/api/endpoints/images';
 
+const log = logger('gallery');
+
 export const addImageRemovedFromBoardFulfilledListener = (startAppListening: AppStartListening) => {
   startAppListening({
     matcher: imagesApi.endpoints.removeImageFromBoard.matchFulfilled,
     effect: (action) => {
-      const log = logger('images');
       const imageDTO = action.meta.arg.originalArgs;
-
       log.debug({ imageDTO }, 'Image removed from board');
     },
   });
@@ -16,9 +16,7 @@ export const addImageRemovedFromBoardFulfilledListener = (startAppListening: App
   startAppListening({
     matcher: imagesApi.endpoints.removeImageFromBoard.matchRejected,
     effect: (action) => {
-      const log = logger('images');
       const imageDTO = action.meta.arg.originalArgs;
-
       log.debug({ imageDTO }, 'Problem removing image from board');
     },
   });
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts
index b0b26fbd1a..e5eaf71c30 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts
@@ -10,11 +10,12 @@ import { omit } from 'lodash-es';
 import { boardsApi } from 'services/api/endpoints/boards';
 import { imagesApi } from 'services/api/endpoints/images';
 
+const log = logger('gallery');
+
 export const addImageUploadedFulfilledListener = (startAppListening: AppStartListening) => {
   startAppListening({
     matcher: imagesApi.endpoints.uploadImage.matchFulfilled,
     effect: (action, { dispatch, getState }) => {
-      const log = logger('images');
       const imageDTO = action.payload;
       const state = getState();
       const { autoAddBoardId } = state.gallery;
@@ -112,7 +113,6 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
   startAppListening({
     matcher: imagesApi.endpoints.uploadImage.matchRejected,
     effect: (action) => {
-      const log = logger('images');
       const sanitizedData = {
         arg: {
           ...omit(action.meta.arg.originalArgs, ['file', 'postUploadAction']),
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts
index bbb9cd1cef..d9c0bf6e67 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts
@@ -1,21 +1,17 @@
 import { logger } from 'app/logging/logger';
 import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
-import {
-  loraDeleted,
-  modelChanged,
-  vaeSelected,
-} from 'features/controlLayers/store/canvasV2Slice';
+import { loraDeleted, modelChanged, vaeSelected } from 'features/controlLayers/store/canvasV2Slice';
 import { modelSelected } from 'features/parameters/store/actions';
 import { zParameterModel } from 'features/parameters/types/parameterSchemas';
 import { toast } from 'features/toast/toast';
 import { t } from 'i18next';
 
+const log = logger('models');
+
 export const addModelSelectedListener = (startAppListening: AppStartListening) => {
   startAppListening({
     actionCreator: modelSelected,
     effect: (action, { getState, dispatch }) => {
-      const log = logger('models');
-
       const state = getState();
       const result = zParameterModel.safeParse(action.payload);
 
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts
index cf3088789c..244f7ebb93 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts
@@ -30,12 +30,13 @@ import {
   isVAEModelConfig,
 } from 'services/api/types';
 
+const log = logger('models');
+
 export const addModelsLoadedListener = (startAppListening: AppStartListening) => {
   startAppListening({
     predicate: modelsApi.endpoints.getModelConfigs.matchFulfilled,
     effect: (action, { getState, dispatch }) => {
       // models loaded, we need to ensure the selected model is available and if not, select the first one
-      const log = logger('models');
       log.info({ models: action.payload.entities }, `Models loaded (${action.payload.ids.length})`);
 
       const state = getState();
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketConnected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketConnected.ts
index babed47d0b..b73ab7e0fa 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketConnected.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketConnected.ts
@@ -8,7 +8,7 @@ import { modelsApi } from 'services/api/endpoints/models';
 import { queueApi, selectQueueStatus } from 'services/api/endpoints/queue';
 import { socketConnected } from 'services/events/setEventListeners';
 
-const log = logger('socketio');
+const log = logger('events');
 
 const $isFirstConnection = atom(true);
 
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts
index 07df2a4f42..834b961ece 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts
@@ -8,11 +8,12 @@ import { getNeedsUpdate, updateNode } from 'features/nodes/util/node/nodeUpdate'
 import { toast } from 'features/toast/toast';
 import { t } from 'i18next';
 
+const log = logger('workflows');
+
 export const addUpdateAllNodesRequestedListener = (startAppListening: AppStartListening) => {
   startAppListening({
     actionCreator: updateAllNodesRequested,
     effect: (action, { dispatch, getState }) => {
-      const log = logger('nodes');
       const { nodes } = getState().nodes.present;
       const templates = $templates.get();
 
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts
index ebce2f80b2..d5e2269cdb 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts
@@ -16,6 +16,8 @@ import type { GraphAndWorkflowResponse, NonNullableGraph } from 'services/api/ty
 import { z } from 'zod';
 import { fromZodError } from 'zod-validation-error';
 
+const log = logger('workflows');
+
 const getWorkflow = async (data: GraphAndWorkflowResponse, templates: Templates) => {
   if (data.workflow) {
     // Prefer to load the workflow if it's available - it has more information
@@ -35,7 +37,6 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
   startAppListening({
     actionCreator: workflowLoadRequested,
     effect: async (action, { dispatch }) => {
-      const log = logger('nodes');
       const { data, asCopy } = action.payload;
       const nodeTemplates = $templates.get();
 
diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts
index fae121c547..88a51b73b9 100644
--- a/invokeai/frontend/web/src/app/store/store.ts
+++ b/invokeai/frontend/web/src/app/store/store.ts
@@ -36,6 +36,8 @@ import { actionsDenylist } from './middleware/devtools/actionsDenylist';
 import { stateSanitizer } from './middleware/devtools/stateSanitizer';
 import { listenerMiddleware } from './middleware/listenerMiddleware';
 
+const log = logger('system');
+
 const allReducers = {
   [api.reducerPath]: api.reducer,
   [gallerySlice.name]: gallerySlice.reducer,
@@ -98,7 +100,6 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
 };
 
 const unserialize: UnserializeFunction = (data, key) => {
-  const log = logger('system');
   const persistConfig = persistConfigs[key as keyof typeof persistConfigs];
   if (!persistConfig) {
     throw new Error(`No persist config for slice "${key}"`);
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts
index 2c31ec174b..11feea75b4 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts
@@ -54,7 +54,6 @@ export class CanvasManager {
   background: CanvasBackground;
 
   log: Logger;
-  workerLog: Logger;
   socket: AppSocket;
 
   _store: AppStore;
@@ -85,7 +84,6 @@ export class CanvasManager {
         },
       };
     });
-    this.workerLog = logger('worker');
 
     this.preview = new CanvasPreview(this);
     this.stage.add(this.preview.getLayer());
@@ -97,9 +95,9 @@ export class CanvasManager {
       const { type, data } = event.data;
       if (type === 'log') {
         if (data.ctx) {
-          this.workerLog[data.level](data.ctx, data.message);
+          this.log[data.level](data.ctx, data.message);
         } else {
-          this.workerLog[data.level](data.message);
+          this.log[data.level](data.message);
         }
       } else if (type === 'extents') {
         const task = this._tasks.get(data.id);
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts
index a55dc3752d..0e2f79a51b 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/events.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/events.ts
@@ -1,7 +1,7 @@
 import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
 import {
   alignCoordForTool,
-  getObjectId,
+  getPrefixedId,
   getScaledCursorPosition,
   offsetCoord,
 } from 'features/controlLayers/konva/util';
@@ -216,7 +216,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
             }
 
             await selectedEntity.adapter.renderer.setBuffer({
-              id: getObjectId('brush_line', true),
+              id: getPrefixedId('brush_line'),
               type: 'brush_line',
               points: [
                 // The last point of the last line is already normalized to the entity's coordinates
@@ -234,7 +234,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
               selectedEntity.adapter.renderer.commitBuffer();
             }
             await selectedEntity.adapter.renderer.setBuffer({
-              id: getObjectId('brush_line', true),
+              id: getPrefixedId('brush_line'),
               type: 'brush_line',
               points: [alignedPoint.x, alignedPoint.y],
               strokeWidth: toolState.brush.width,
@@ -254,7 +254,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
               selectedEntity.adapter.renderer.commitBuffer();
             }
             await selectedEntity.adapter.renderer.setBuffer({
-              id: getObjectId('eraser_line', true),
+              id: getPrefixedId('eraser_line'),
               type: 'eraser_line',
               points: [
                 // The last point of the last line is already normalized to the entity's coordinates
@@ -271,7 +271,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
               selectedEntity.adapter.renderer.commitBuffer();
             }
             await selectedEntity.adapter.renderer.setBuffer({
-              id: getObjectId('eraser_line', true),
+              id: getPrefixedId('eraser_line'),
               type: 'eraser_line',
               points: [alignedPoint.x, alignedPoint.y],
               strokeWidth: toolState.eraser.width,
@@ -286,7 +286,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
             selectedEntity.adapter.renderer.commitBuffer();
           }
           await selectedEntity.adapter.renderer.setBuffer({
-            id: getObjectId('rect', true),
+            id: getPrefixedId('rect'),
             type: 'rect',
             rect: { x: Math.round(normalizedPoint.x), y: Math.round(normalizedPoint.y), width: 0, height: 0 },
             color: getCurrentFill(),
@@ -375,7 +375,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
             const normalizedPoint = offsetCoord(pos, selectedEntity.state.position);
             const alignedPoint = alignCoordForTool(normalizedPoint, toolState.brush.width);
             await selectedEntity.adapter.renderer.setBuffer({
-              id: getObjectId('brush_line', true),
+              id: getPrefixedId('brush_line'),
               type: 'brush_line',
               points: [alignedPoint.x, alignedPoint.y],
               strokeWidth: toolState.brush.width,
@@ -412,7 +412,7 @@ export const setStageEventHandlers = (manager: CanvasManager): (() => void) => {
             const normalizedPoint = offsetCoord(pos, selectedEntity.state.position);
             const alignedPoint = alignCoordForTool(normalizedPoint, toolState.eraser.width);
             await selectedEntity.adapter.renderer.setBuffer({
-              id: getObjectId('eraser_line', true),
+              id: getPrefixedId('eraser_line'),
               type: 'eraser_line',
               points: [alignedPoint.x, alignedPoint.y],
               strokeWidth: toolState.eraser.width,
diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts
index 092ca853fb..e1e66ac07d 100644
--- a/invokeai/frontend/web/src/features/controlLayers/konva/util.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/konva/util.ts
@@ -1,4 +1,4 @@
-import type { CanvasObjectState, Coordinate, Rect, RgbaColor } from 'features/controlLayers/store/types';
+import type { Coordinate, Rect, RgbaColor } from 'features/controlLayers/store/types';
 import Konva from 'konva';
 import type { KonvaEventObject } from 'konva/lib/Node';
 import type { Vector2d } from 'konva/lib/types';
@@ -363,14 +363,6 @@ export function getPrefixedId(prefix: string): string {
   return `${prefix}:${nanoid()}`;
 }
 
-export function getObjectId(type: CanvasObjectState['type'], isBuffer?: boolean): string {
-  if (isBuffer) {
-    return getPrefixedId(`buffer_${type}`);
-  } else {
-    return getPrefixedId(type);
-  }
-}
-
 export const getEmptyRect = (): Rect => {
   return { x: 0, y: 0, width: 0, height: 0 };
 };
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
index ab160fd433..aec0ca9891 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts
@@ -1,7 +1,7 @@
 import type { CanvasControlAdapter } from 'features/controlLayers/konva/CanvasControlAdapter';
 import { CanvasLayerAdapter } from 'features/controlLayers/konva/CanvasLayerAdapter';
 import { CanvasMaskAdapter } from 'features/controlLayers/konva/CanvasMaskAdapter';
-import { getObjectId } from 'features/controlLayers/konva/util';
+import { getPrefixedId } from 'features/controlLayers/konva/util';
 import { zModelIdentifierField } from 'features/nodes/types/common';
 import type { AspectRatioState } from 'features/parameters/components/DocumentSize/types';
 import type {
@@ -803,7 +803,7 @@ export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO)
 export const imageDTOToImageObject = (imageDTO: ImageDTO, overrides?: Partial<CanvasImageState>): CanvasImageState => {
   const { width, height, image_name } = imageDTO;
   return {
-    id: getObjectId('image'),
+    id: getPrefixedId('image'),
     type: 'image',
     image: {
       image_name,
diff --git a/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx b/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx
index f800e5c869..49af67696c 100644
--- a/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx
+++ b/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx
@@ -11,23 +11,21 @@ import { memo, useCallback, useState } from 'react';
 
 import { DndContextTypesafe } from './DndContextTypesafe';
 
+const log = logger('system');
+
 const AppDndContext = (props: PropsWithChildren) => {
   const [activeDragData, setActiveDragData] = useState<TypesafeDraggableData | null>(null);
-  const log = logger('images');
 
   const dispatch = useAppDispatch();
 
-  const handleDragStart = useCallback(
-    (event: DragStartEvent) => {
-      log.trace({ dragData: parseify(event.active.data.current) }, 'Drag started');
-      const activeData = event.active.data.current;
-      if (!activeData) {
-        return;
-      }
-      setActiveDragData(activeData);
-    },
-    [log]
-  );
+  const handleDragStart = useCallback((event: DragStartEvent) => {
+    log.trace({ dragData: parseify(event.active.data.current) }, 'Drag started');
+    const activeData = event.active.data.current;
+    if (!activeData) {
+      return;
+    }
+    setActiveDragData(activeData);
+  }, []);
 
   const handleDragEnd = useCallback(
     (event: DragEndEvent) => {
@@ -39,7 +37,7 @@ const AppDndContext = (props: PropsWithChildren) => {
       dispatch(dndDropped({ overData, activeData: activeDragData }));
       setActiveDragData(null);
     },
-    [activeDragData, dispatch, log]
+    [activeDragData, dispatch]
   );
 
   const mouseSensor = useSensor(MouseSensor, {
diff --git a/invokeai/frontend/web/src/features/metadata/util/recallers.ts b/invokeai/frontend/web/src/features/metadata/util/recallers.ts
index c1bf41d1f4..72c7249533 100644
--- a/invokeai/frontend/web/src/features/metadata/util/recallers.ts
+++ b/invokeai/frontend/web/src/features/metadata/util/recallers.ts
@@ -15,14 +15,14 @@ import {
   bboxWidthChanged,
   // caRecalled,
   ipaRecalled,
-  rasterLayerAllDeleted,
-  rasterLayerRecalled,
   loraAllDeleted,
   loraRecalled,
   negativePrompt2Changed,
   negativePromptChanged,
   positivePrompt2Changed,
   positivePromptChanged,
+  rasterLayerAllDeleted,
+  rasterLayerRecalled,
   refinerModelChanged,
   rgRecalled,
   setCfgRescaleMultiplier,
@@ -80,6 +80,8 @@ import type {
 import { getImageDTO } from 'services/api/endpoints/images';
 import { v4 as uuidv4 } from 'uuid';
 
+const log = logger('metadata');
+
 const recallPositivePrompt: MetadataRecallFunc<ParameterPositivePrompt> = (positivePrompt) => {
   getStore().dispatch(positivePromptChanged(positivePrompt));
 };
@@ -351,7 +353,7 @@ const recallLayer: MetadataRecallFunc<CanvasRasterLayerState> = async (layer) =>
     } else if (obj.type === 'rect') {
       obj.id = getRectShapeId(clone.id, uuidv4());
     } else {
-      logger('metadata').error(`Unknown object type ${obj.type}`);
+      log.error(`Unknown object type ${obj.type}`);
     }
   }
   clone.id = getRGId(uuidv4());
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
index 2471d24e06..580fee42b0 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSD1Graph.ts
@@ -34,6 +34,7 @@ import { isNonRefinerMainModelConfig } from 'services/api/types';
 import { assert } from 'tsafe';
 
 import { addRegions } from './addRegions';
+
 const log = logger('system');
 
 export const buildSD1Graph = async (state: RootState, manager: CanvasManager): Promise<Graph> => {
diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
index 94a195b7d4..7a30befb42 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graph/generation/buildSDXLGraph.ts
@@ -33,6 +33,7 @@ import { isNonRefinerMainModelConfig } from 'services/api/types';
 import { assert } from 'tsafe';
 
 import { addRegions } from './addRegions';
+
 const log = logger('system');
 
 export const buildSDXLGraph = async (state: RootState, manager: CanvasManager): Promise<Graph> => {
diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts b/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts
index 3981b759db..77b94bba9d 100644
--- a/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts
@@ -25,6 +25,8 @@ import { buildFieldInputTemplate } from './buildFieldInputTemplate';
 import { buildFieldOutputTemplate } from './buildFieldOutputTemplate';
 import { isCollectionFieldType, parseFieldType } from './parseFieldType';
 
+const log = logger('system');
+
 const RESERVED_INPUT_FIELD_NAMES = ['id', 'type', 'use_cache'];
 const RESERVED_OUTPUT_FIELD_NAMES = ['type'];
 const RESERVED_FIELD_TYPES = ['IsIntermediate'];
@@ -85,18 +87,12 @@ export const parseSchema = (
       schema.properties,
       (inputsAccumulator: Record<string, FieldInputTemplate>, property, propertyName) => {
         if (isReservedInputField(type, propertyName)) {
-          logger('nodes').trace(
-            { node: type, field: propertyName, schema: parseify(property) },
-            'Skipped reserved input field'
-          );
+          log.trace({ node: type, field: propertyName, schema: parseify(property) }, 'Skipped reserved input field');
           return inputsAccumulator;
         }
 
         if (!isInvocationFieldSchema(property)) {
-          logger('nodes').warn(
-            { node: type, field: propertyName, schema: parseify(property) },
-            'Unhandled input property'
-          );
+          log.warn({ node: type, field: propertyName, schema: parseify(property) }, 'Unhandled input property');
           return inputsAccumulator;
         }
 
@@ -111,18 +107,12 @@ export const parseSchema = (
 
         const fieldType = fieldTypeOverride ?? originalFieldType;
         if (!fieldType) {
-          logger('nodes').trace(
-            { node: type, field: propertyName, schema: parseify(property) },
-            'Unable to parse field type'
-          );
+          log.trace({ node: type, field: propertyName, schema: parseify(property) }, 'Unable to parse field type');
           return inputsAccumulator;
         }
 
         if (isReservedFieldType(fieldType.name)) {
-          logger('nodes').trace(
-            { node: type, field: propertyName, schema: parseify(property) },
-            'Skipped reserved input field'
-          );
+          log.trace({ node: type, field: propertyName, schema: parseify(property) }, 'Skipped reserved input field');
           return inputsAccumulator;
         }
 
@@ -141,18 +131,18 @@ export const parseSchema = (
     const outputSchemaName = schema.output.$ref.split('/').pop();
 
     if (!outputSchemaName) {
-      logger('nodes').warn({ outputRefObject: parseify(schema.output) }, 'No output schema name found in ref object');
+      log.warn({ outputRefObject: parseify(schema.output) }, 'No output schema name found in ref object');
       return invocationsAccumulator;
     }
 
     const outputSchema = openAPI.components?.schemas?.[outputSchemaName];
     if (!outputSchema) {
-      logger('nodes').warn({ outputSchemaName }, 'Output schema not found');
+      log.warn({ outputSchemaName }, 'Output schema not found');
       return invocationsAccumulator;
     }
 
     if (!isInvocationOutputSchemaObject(outputSchema)) {
-      logger('nodes').error({ outputSchema: parseify(outputSchema) }, 'Invalid output schema');
+      log.error({ outputSchema: parseify(outputSchema) }, 'Invalid output schema');
       return invocationsAccumulator;
     }
 
@@ -162,18 +152,12 @@ export const parseSchema = (
       outputSchema.properties,
       (outputsAccumulator, property, propertyName) => {
         if (!isAllowedOutputField(type, propertyName)) {
-          logger('nodes').trace(
-            { node: type, field: propertyName, schema: parseify(property) },
-            'Skipped reserved output field'
-          );
+          log.trace({ node: type, field: propertyName, schema: parseify(property) }, 'Skipped reserved output field');
           return outputsAccumulator;
         }
 
         if (!isInvocationFieldSchema(property)) {
-          logger('nodes').warn(
-            { node: type, field: propertyName, schema: parseify(property) },
-            'Unhandled output property'
-          );
+          log.warn({ node: type, field: propertyName, schema: parseify(property) }, 'Unhandled output property');
           return outputsAccumulator;
         }
 
@@ -188,10 +172,7 @@ export const parseSchema = (
 
         const fieldType = fieldTypeOverride ?? originalFieldType;
         if (!fieldType) {
-          logger('nodes').trace(
-            { node: type, field: propertyName, schema: parseify(property) },
-            'Unable to parse field type'
-          );
+          log.trace({ node: type, field: propertyName, schema: parseify(property) }, 'Unable to parse field type');
           return outputsAccumulator;
         }
 
@@ -242,7 +223,7 @@ const getFieldType = (
   } catch (e) {
     const tKey = kind === 'input' ? 'nodes.inputFieldTypeParseError' : 'nodes.outputFieldTypeParseError';
     if (e instanceof FieldParseError) {
-      logger('nodes').warn(
+      log.warn(
         {
           node: type,
           field: propertyName,
@@ -255,7 +236,7 @@ const getFieldType = (
         })
       );
     } else {
-      logger('nodes').warn(
+      log.warn(
         {
           node: type,
           field: propertyName,
diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts
index cec8b0a2b7..ba01b258f2 100644
--- a/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts
@@ -9,6 +9,8 @@ import i18n from 'i18n';
 import { pick } from 'lodash-es';
 import { fromZodError } from 'zod-validation-error';
 
+const log = logger('workflows');
+
 export type BuildWorkflowArg = {
   nodes: NodesState['nodes'];
   edges: NodesState['edges'];
@@ -93,7 +95,7 @@ export const buildWorkflowWithValidation = ({ nodes, edges, workflow }: BuildWor
       prefix: i18n.t('nodes.unableToValidateWorkflow'),
     });
 
-    logger('nodes').warn({ workflow: parseify(workflowToValidate) }, message);
+    log.warn({ workflow: parseify(workflowToValidate) }, message);
     return null;
   }
 
diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/graphToWorkflow.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/graphToWorkflow.ts
index af66d3cc6b..6560977ece 100644
--- a/invokeai/frontend/web/src/features/nodes/util/workflow/graphToWorkflow.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/workflow/graphToWorkflow.ts
@@ -9,6 +9,8 @@ import { forEach } from 'lodash-es';
 import type { NonNullableGraph } from 'services/api/types';
 import { v4 as uuidv4 } from 'uuid';
 
+const log = logger('workflows');
+
 /**
  * Converts a graph to a workflow. This is a best-effort conversion and may not be perfect.
  * For example, if a graph references an unknown node type, that node will be skipped.
@@ -43,7 +45,7 @@ export const graphToWorkflow = (graph: NonNullableGraph, autoLayout = true): Wor
 
     // Skip missing node templates - this is a best-effort
     if (!template) {
-      logger('nodes').warn(`Node type ${node.type} not found in templates`);
+      log.warn(`Node type ${node.type} not found in templates`);
       return;
     }
 
@@ -60,7 +62,7 @@ export const graphToWorkflow = (graph: NonNullableGraph, autoLayout = true): Wor
 
       // Skip missing input templates
       if (!inputTemplate) {
-        logger('nodes').warn(`Input ${key} not found in template for node type ${node.type}`);
+        log.warn(`Input ${key} not found in template for node type ${node.type}`);
         return;
       }
 
diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperContent.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperContent.tsx
new file mode 100644
index 0000000000..9269132f71
--- /dev/null
+++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperContent.tsx
@@ -0,0 +1,17 @@
+import { Flex } from '@invoke-ai/ui-library';
+import { SettingsDeveloperLogIsEnabled } from 'features/system/components/SettingsModal/SettingsDeveloperLogIsEnabled';
+import { SettingsDeveloperLogLevel } from 'features/system/components/SettingsModal/SettingsDeveloperLogLevel';
+import { SettingsDeveloperLogNamespaces } from 'features/system/components/SettingsModal/SettingsDeveloperLogNamespaces';
+import { memo } from 'react';
+
+export const SettingsDeveloperContent = memo(() => {
+  return (
+    <Flex flexDir="column" gap={4}>
+      <SettingsDeveloperLogIsEnabled />
+      <SettingsDeveloperLogLevel />
+      <SettingsDeveloperLogNamespaces />
+    </Flex>
+  );
+});
+
+SettingsDeveloperContent.displayName = 'SettingsDeveloperContent';
diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperLogIsEnabled.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperLogIsEnabled.tsx
new file mode 100644
index 0000000000..315546d3da
--- /dev/null
+++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperLogIsEnabled.tsx
@@ -0,0 +1,29 @@
+import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
+import { useAppSelector } from 'app/store/storeHooks';
+import { logIsEnabledChanged } from 'features/system/store/systemSlice';
+import type { ChangeEvent } from 'react';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useDispatch } from 'react-redux';
+
+export const SettingsDeveloperLogIsEnabled = memo(() => {
+  const { t } = useTranslation();
+  const dispatch = useDispatch();
+  const logIsEnabled = useAppSelector((s) => s.system.logIsEnabled);
+
+  const onChangeLogIsEnabled = useCallback(
+    (e: ChangeEvent<HTMLInputElement>) => {
+      dispatch(logIsEnabledChanged(e.target.checked));
+    },
+    [dispatch]
+  );
+
+  return (
+    <FormControl>
+      <FormLabel>{t('system.enableLogging')}</FormLabel>
+      <Switch isChecked={logIsEnabled} onChange={onChangeLogIsEnabled} />
+    </FormControl>
+  );
+});
+
+SettingsDeveloperLogIsEnabled.displayName = 'SettingsDeveloperLogIsEnabled';
diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperLogLevel.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperLogLevel.tsx
new file mode 100644
index 0000000000..06d05b7761
--- /dev/null
+++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperLogLevel.tsx
@@ -0,0 +1,34 @@
+import type { ComboboxOnChange } from '@invoke-ai/ui-library';
+import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
+import { isLogLevel, zLogLevel } from 'app/logging/logger';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { logLevelChanged } from 'features/system/store/systemSlice';
+import { memo, useCallback, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+export const SettingsDeveloperLogLevel = memo(() => {
+  const { t } = useTranslation();
+  const dispatch = useAppDispatch();
+  const logLevel = useAppSelector((s) => s.system.logLevel);
+  const options = useMemo(() => zLogLevel.options.map((o) => ({ label: t(`system.logLevel.${o}`), value: o })), [t]);
+
+  const value = useMemo(() => options.find((o) => o.value === logLevel), [logLevel, options]);
+
+  const onChange = useCallback<ComboboxOnChange>(
+    (v) => {
+      if (!isLogLevel(v?.value)) {
+        return;
+      }
+      dispatch(logLevelChanged(v.value));
+    },
+    [dispatch]
+  );
+  return (
+    <FormControl>
+      <FormLabel>{t('system.logLevel.logLevel')}</FormLabel>
+      <Combobox value={value} options={options} onChange={onChange} isSearchable={false} />
+    </FormControl>
+  );
+});
+
+SettingsDeveloperLogLevel.displayName = 'SettingsDeveloperLogLevel';
diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperLogNamespaces.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperLogNamespaces.tsx
new file mode 100644
index 0000000000..a0f4b63aff
--- /dev/null
+++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperLogNamespaces.tsx
@@ -0,0 +1,70 @@
+import { Flex, FormControl, FormLabel, Tag, TagCloseButton, Text } from '@invoke-ai/ui-library';
+import type { LogNamespace } from 'app/logging/logger';
+import { zLogNamespace } from 'app/logging/logger';
+import { EMPTY_ARRAY } from 'app/store/constants';
+import { useAppSelector } from 'app/store/storeHooks';
+import { logNamespaceToggled } from 'features/system/store/systemSlice';
+import { difference } from 'lodash-es';
+import { memo, useCallback, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useDispatch } from 'react-redux';
+
+export const SettingsDeveloperLogNamespaces = memo(() => {
+  const { t } = useTranslation();
+  const enabledLogNamespaces = useAppSelector((s) => {
+    if (s.system.logNamespaces.length === 0) {
+      return EMPTY_ARRAY;
+    } else {
+      return s.system.logNamespaces.toSorted();
+    }
+  });
+  const disabledLogNamespaces = useMemo(
+    () => difference(zLogNamespace.options, enabledLogNamespaces).toSorted(),
+    [enabledLogNamespaces]
+  );
+
+  return (
+    <FormControl orientation="vertical">
+      <FormLabel>{t('system.logNamespaces.logNamespaces')}</FormLabel>
+      <Flex w="full" gap={2} flexWrap="wrap" minH="32px" borderRadius="base" borderWidth={1} p={2} alignItems="center">
+        {enabledLogNamespaces.map((namespace) => (
+          <LogLevelTag key={`enabled-${namespace}`} namespace={namespace} isEnabled={true} />
+        ))}
+      </Flex>
+      <Flex gap={2} flexWrap="wrap">
+        {disabledLogNamespaces.map((namespace) => (
+          <LogLevelTag key={`disabled-${namespace}`} namespace={namespace} isEnabled={false} />
+        ))}
+      </Flex>
+    </FormControl>
+  );
+});
+
+SettingsDeveloperLogNamespaces.displayName = 'SettingsDeveloperLogNamespaces';
+
+const LogLevelTag = ({ namespace, isEnabled }: { namespace: LogNamespace; isEnabled: boolean }) => {
+  const { t } = useTranslation();
+  const dispatch = useDispatch();
+  const onClick = useCallback(() => {
+    dispatch(logNamespaceToggled(namespace));
+  }, [dispatch, namespace]);
+
+  return (
+    <Tag
+      h="min-content"
+      borderRadius="base"
+      onClick={onClick}
+      colorScheme={isEnabled ? 'invokeBlue' : 'base'}
+      userSelect="none"
+      role="button"
+      size="md"
+      color="base.900"
+      bg={isEnabled ? 'invokeBlue.300' : 'base.300'}
+    >
+      <Text fontSize="sm" fontWeight="semibold">
+        {t(`system.logNamespaces.${namespace}`)}
+      </Text>
+      {isEnabled && <TagCloseButton />}
+    </Tag>
+  );
+};
diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsLogLevelSelect.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsLogLevelSelect.tsx
index 7a3eb06992..ae05a1471a 100644
--- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsLogLevelSelect.tsx
+++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsLogLevelSelect.tsx
@@ -2,30 +2,30 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library';
 import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
 import { isLogLevel, zLogLevel } from 'app/logging/logger';
 import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { consoleLogLevelChanged } from 'features/system/store/systemSlice';
+import { logLevelChanged } from 'features/system/store/systemSlice';
 import { memo, useCallback, useMemo } from 'react';
 import { useTranslation } from 'react-i18next';
 
 export const SettingsLogLevelSelect = memo(() => {
   const { t } = useTranslation();
   const dispatch = useAppDispatch();
-  const consoleLogLevel = useAppSelector((s) => s.system.consoleLogLevel);
-  const shouldLogToConsole = useAppSelector((s) => s.system.shouldLogToConsole);
+  const logLevel = useAppSelector((s) => s.system.logLevel);
+  const logIsEnabled = useAppSelector((s) => s.system.logIsEnabled);
   const options = useMemo(() => zLogLevel.options.map((o) => ({ label: o, value: o })), []);
 
-  const value = useMemo(() => options.find((o) => o.value === consoleLogLevel), [consoleLogLevel, options]);
+  const value = useMemo(() => options.find((o) => o.value === logLevel), [logLevel, options]);
 
   const onChange = useCallback<ComboboxOnChange>(
     (v) => {
       if (!isLogLevel(v?.value)) {
         return;
       }
-      dispatch(consoleLogLevelChanged(v.value));
+      dispatch(logLevelChanged(v.value));
     },
     [dispatch]
   );
   return (
-    <FormControl isDisabled={!shouldLogToConsole}>
+    <FormControl isDisabled={!logIsEnabled}>
       <FormLabel>{t('common.loglevel')}</FormLabel>
       <Combobox value={value} options={options} onChange={onChange} />
     </FormControl>
diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx
index 1853183c9b..7f72eb37a7 100644
--- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx
+++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx
@@ -20,14 +20,16 @@ import { InformationalPopover } from 'common/components/InformationalPopover/Inf
 import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
 import { useClearStorage } from 'common/hooks/useClearStorage';
 import { shouldUseCpuNoiseChanged } from 'features/controlLayers/store/canvasV2Slice';
+import { SettingsDeveloperLogIsEnabled } from 'features/system/components/SettingsModal/SettingsDeveloperLogIsEnabled';
+import { SettingsDeveloperLogLevel } from 'features/system/components/SettingsModal/SettingsDeveloperLogLevel';
+import { SettingsDeveloperLogNamespaces } from 'features/system/components/SettingsModal/SettingsDeveloperLogNamespaces';
 import { useClearIntermediates } from 'features/system/components/SettingsModal/useClearIntermediates';
 import { StickyScrollable } from 'features/system/components/StickyScrollable';
 import {
-  setEnableImageDebugging,
+  logIsEnabledChanged,
   setShouldConfirmOnDelete,
   setShouldEnableInformationalPopovers,
   shouldAntialiasProgressImageChanged,
-  shouldLogToConsoleChanged,
   shouldUseNSFWCheckerChanged,
   shouldUseWatermarkerChanged,
 } from 'features/system/store/systemSlice';
@@ -38,7 +40,6 @@ import { useTranslation } from 'react-i18next';
 import { useGetAppConfigQuery } from 'services/api/endpoints/appInfo';
 
 import { SettingsLanguageSelect } from './SettingsLanguageSelect';
-import { SettingsLogLevelSelect } from './SettingsLogLevelSelect';
 
 type ConfigOptions = {
   shouldShowDeveloperSettings?: boolean;
@@ -65,7 +66,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
 
   useEffect(() => {
     if (!shouldShowDeveloperSettings) {
-      dispatch(shouldLogToConsoleChanged(false));
+      dispatch(logIsEnabledChanged(false));
     }
   }, [shouldShowDeveloperSettings, dispatch]);
 
@@ -90,9 +91,7 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
 
   const shouldUseCpuNoise = useAppSelector((s) => s.canvasV2.params.shouldUseCpuNoise);
   const shouldConfirmOnDelete = useAppSelector((s) => s.system.shouldConfirmOnDelete);
-  const enableImageDebugging = useAppSelector((s) => s.system.enableImageDebugging);
   const shouldShowProgressInViewer = useAppSelector((s) => s.ui.shouldShowProgressInViewer);
-  const shouldLogToConsole = useAppSelector((s) => s.system.shouldLogToConsole);
   const shouldAntialiasProgressImage = useAppSelector((s) => s.system.shouldAntialiasProgressImage);
   const shouldUseNSFWChecker = useAppSelector((s) => s.system.shouldUseNSFWChecker);
   const shouldUseWatermarker = useAppSelector((s) => s.system.shouldUseWatermarker);
@@ -120,13 +119,6 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
     }
   }, [countdown]);
 
-  const handleLogToConsoleChanged = useCallback(
-    (e: ChangeEvent<HTMLInputElement>) => {
-      dispatch(shouldLogToConsoleChanged(e.target.checked));
-    },
-    [dispatch]
-  );
-
   const handleChangeShouldConfirmOnDelete = useCallback(
     (e: ChangeEvent<HTMLInputElement>) => {
       dispatch(setShouldConfirmOnDelete(e.target.checked));
@@ -163,12 +155,6 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
     },
     [dispatch]
   );
-  const handleChangeEnableImageDebugging = useCallback(
-    (e: ChangeEvent<HTMLInputElement>) => {
-      dispatch(setEnableImageDebugging(e.target.checked));
-    },
-    [dispatch]
-  );
   const handleChangeShouldUseCpuNoise = useCallback(
     (e: ChangeEvent<HTMLInputElement>) => {
       dispatch(shouldUseCpuNoiseChanged(e.target.checked));
@@ -242,15 +228,9 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => {
 
                   {shouldShowDeveloperSettings && (
                     <StickyScrollable title={t('settings.developer')}>
-                      <FormControl>
-                        <FormLabel>{t('settings.shouldLogToConsole')}</FormLabel>
-                        <Switch isChecked={shouldLogToConsole} onChange={handleLogToConsoleChanged} />
-                      </FormControl>
-                      <SettingsLogLevelSelect />
-                      <FormControl>
-                        <FormLabel>{t('settings.enableImageDebugging')}</FormLabel>
-                        <Switch isChecked={enableImageDebugging} onChange={handleChangeEnableImageDebugging} />
-                      </FormControl>
+                      <SettingsDeveloperLogIsEnabled />
+                      <SettingsDeveloperLogLevel />
+                      <SettingsDeveloperLogNamespaces />
                     </StickyScrollable>
                   )}
 
diff --git a/invokeai/frontend/web/src/features/system/store/systemSlice.ts b/invokeai/frontend/web/src/features/system/store/systemSlice.ts
index 0b6131c92b..44b5af7ea4 100644
--- a/invokeai/frontend/web/src/features/system/store/systemSlice.ts
+++ b/invokeai/frontend/web/src/features/system/store/systemSlice.ts
@@ -1,21 +1,23 @@
 import type { PayloadAction } from '@reduxjs/toolkit';
 import { createSlice } from '@reduxjs/toolkit';
+import type { LogNamespace } from 'app/logging/logger';
+import { zLogNamespace } from 'app/logging/logger';
 import type { PersistConfig, RootState } from 'app/store/store';
-import type { LogLevelName } from 'roarr';
+import { uniq } from 'lodash-es';
 
 import type { Language, SystemState } from './types';
 
 const initialSystemState: SystemState = {
   _version: 1,
   shouldConfirmOnDelete: true,
-  enableImageDebugging: false,
   shouldAntialiasProgressImage: false,
-  consoleLogLevel: 'debug',
-  shouldLogToConsole: true,
   language: 'en',
   shouldUseNSFWChecker: false,
   shouldUseWatermarker: false,
   shouldEnableInformationalPopovers: true,
+  logIsEnabled: true,
+  logLevel: 'debug',
+  logNamespaces: [...zLogNamespace.options],
 };
 
 export const systemSlice = createSlice({
@@ -25,14 +27,18 @@ export const systemSlice = createSlice({
     setShouldConfirmOnDelete: (state, action: PayloadAction<boolean>) => {
       state.shouldConfirmOnDelete = action.payload;
     },
-    setEnableImageDebugging: (state, action: PayloadAction<boolean>) => {
-      state.enableImageDebugging = action.payload;
+    logIsEnabledChanged: (state, action: PayloadAction<SystemState['logIsEnabled']>) => {
+      state.logIsEnabled = action.payload;
     },
-    consoleLogLevelChanged: (state, action: PayloadAction<LogLevelName>) => {
-      state.consoleLogLevel = action.payload;
+    logLevelChanged: (state, action: PayloadAction<SystemState['logLevel']>) => {
+      state.logLevel = action.payload;
     },
-    shouldLogToConsoleChanged: (state, action: PayloadAction<boolean>) => {
-      state.shouldLogToConsole = action.payload;
+    logNamespaceToggled: (state, action: PayloadAction<LogNamespace>) => {
+      if (state.logNamespaces.includes(action.payload)) {
+        state.logNamespaces = uniq(state.logNamespaces.filter((n) => n !== action.payload));
+      } else {
+        state.logNamespaces = uniq([...state.logNamespaces, action.payload]);
+      }
     },
     shouldAntialiasProgressImageChanged: (state, action: PayloadAction<boolean>) => {
       state.shouldAntialiasProgressImage = action.payload;
@@ -54,9 +60,9 @@ export const systemSlice = createSlice({
 
 export const {
   setShouldConfirmOnDelete,
-  setEnableImageDebugging,
-  consoleLogLevelChanged,
-  shouldLogToConsoleChanged,
+  logIsEnabledChanged,
+  logLevelChanged,
+  logNamespaceToggled,
   shouldAntialiasProgressImageChanged,
   languageChanged,
   shouldUseNSFWCheckerChanged,
diff --git a/invokeai/frontend/web/src/features/system/store/types.ts b/invokeai/frontend/web/src/features/system/store/types.ts
index 6aa200ebaa..10d559690d 100644
--- a/invokeai/frontend/web/src/features/system/store/types.ts
+++ b/invokeai/frontend/web/src/features/system/store/types.ts
@@ -1,4 +1,4 @@
-import type { LogLevel } from 'app/logging/logger';
+import type { LogLevel, LogNamespace } from 'app/logging/logger';
 import { z } from 'zod';
 
 const zLanguage = z.enum([
@@ -31,12 +31,12 @@ export const isLanguage = (v: unknown): v is Language => zLanguage.safeParse(v).
 export interface SystemState {
   _version: 1;
   shouldConfirmOnDelete: boolean;
-  enableImageDebugging: boolean;
-  consoleLogLevel: LogLevel;
-  shouldLogToConsole: boolean;
   shouldAntialiasProgressImage: boolean;
   language: Language;
   shouldUseNSFWChecker: boolean;
   shouldUseWatermarker: boolean;
   shouldEnableInformationalPopovers: boolean;
+  logIsEnabled: boolean;
+  logLevel: LogLevel;
+  logNamespaces: LogNamespace[];
 }
diff --git a/invokeai/frontend/web/src/services/events/setEventListeners.tsx b/invokeai/frontend/web/src/services/events/setEventListeners.tsx
index 213249da99..3c08c7953e 100644
--- a/invokeai/frontend/web/src/services/events/setEventListeners.tsx
+++ b/invokeai/frontend/web/src/services/events/setEventListeners.tsx
@@ -27,7 +27,7 @@ import type { Socket } from 'socket.io-client';
 
 export const socketConnected = createAction('socket/connected');
 
-const log = logger('socketio');
+const log = logger('events');
 
 type SetEventListenersArg = {
   socket: Socket<ServerToClientEvents, ClientToServerEvents>;