From f9bf0787b9279c7690d27524e767ea180c1ba5aa Mon Sep 17 00:00:00 2001
From: Lee Rowlands <lee.rowlands@previousnext.com.au>
Date: Fri, 28 Apr 2023 13:36:57 +1000
Subject: [PATCH] Issue #2932518 by kim.pepper, joachim, bradjones1, voleger,
 cliddell, gapple, Xano, andregp, andypost, neclimdul, rpayanm,
 Hardik_Patel_12, NWOM, smustgrave, dawehner, alexpott, daffie, larowlan,
 Berdir, mstrelan, xjm, dagmar: Deprecate watchdog_exception

---
 core/core.services.yml                        | 12 ++-
 core/includes/bootstrap.inc                   |  6 ++
 core/includes/update.inc                      |  7 +-
 core/lib/Drupal/Core/Cron.php                 |  9 ++-
 core/lib/Drupal/Core/Database/Transaction.php |  6 +-
 .../lib/Drupal/Core/Database/database.api.php |  4 +-
 .../Entity/Sql/SqlContentEntityStorage.php    | 11 +--
 .../MenuRouterRebuildSubscriber.php           | 14 +++-
 .../Drupal/Core/Extension/ModuleInstaller.php | 12 ++-
 .../lib/Drupal/Core/Routing/MatcherDumper.php | 28 ++++++-
 core/lib/Drupal/Core/Utility/Error.php        | 20 +++++
 core/modules/sdc/sdc.services.yml             |  1 +
 .../sdc/src/Twig/TwigComponentLoader.php      | 11 ++-
 .../SecurityAdvisoriesFetcher.php             |  5 +-
 core/modules/system/system.install            |  3 +-
 .../SecurityAdvisoriesFetcherTest.php         |  8 +-
 .../update/src/ProjectSecurityData.php        |  8 +-
 core/modules/update/src/UpdateFetcher.php     | 12 ++-
 .../tests/src/Unit/UpdateFetcherTest.php      | 48 +++++-------
 core/modules/update/update.compare.inc        |  8 +-
 core/modules/update/update.services.yml       |  5 +-
 .../workspaces/src/WorkspaceAssociation.php   | 12 ++-
 .../workspaces/src/WorkspaceMerger.php        | 12 ++-
 .../src/WorkspaceOperationFactory.php         | 13 +++-
 .../workspaces/src/WorkspacePublisher.php     | 12 ++-
 .../workspaces/workspaces.services.yml        |  4 +-
 .../Core/Bootstrap/LegacyBootstrapTest.php    | 37 ++++++++++
 .../Core/Routing/LegacyMatcherDumperTest.php  | 74 +++++++++++++++++++
 .../Core/Routing/MatcherDumperTest.php        | 23 ++++--
 .../Core/Routing/RouteProviderTest.php        | 40 ++++++----
 .../Core/Cron/CronSuspendQueueDelayTest.php   |  2 +-
 31 files changed, 347 insertions(+), 120 deletions(-)
 create mode 100644 core/tests/Drupal/KernelTests/Core/Bootstrap/LegacyBootstrapTest.php
 create mode 100644 core/tests/Drupal/KernelTests/Core/Routing/LegacyMatcherDumperTest.php

diff --git a/core/core.services.yml b/core/core.services.yml
index 18b227280378..ed531f502701 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -494,6 +494,12 @@ services:
   logger.channel.security:
     parent: logger.channel_base
     arguments: ['security']
+  logger.channel.menu:
+    parent: logger.channel_base
+    arguments: ['menu']
+  logger.channel.router:
+    parent: logger.channel_base
+    arguments: ['router']
   logger.log_message_parser:
     class: Drupal\Core\Logger\LogMessageParser
   Drupal\Core\Logger\LogMessageParserInterface: '@logger.log_message_parser'
@@ -586,7 +592,7 @@ services:
     class: Drupal\Core\Extension\ModuleInstaller
     tags:
       - { name: service_collector, tag: 'module_install.uninstall_validator', call: addUninstallValidator }
-    arguments: ['%app.root%', '@module_handler', '@kernel', '@database', '@update.update_hook_registry']
+    arguments: ['%app.root%', '@module_handler', '@kernel', '@database', '@update.update_hook_registry', '@logger.channel.default']
     lazy: true
   Drupal\Core\Extension\ModuleInstallerInterface: '@module_installer'
   extension.list.module:
@@ -1055,7 +1061,7 @@ services:
     arguments: ['@keyvalue']
   router.dumper:
     class: Drupal\Core\Routing\MatcherDumper
-    arguments: ['@database', '@state']
+    arguments: ['@database', '@state', '@logger.channel.router']
     tags:
       - { name: backend_overridable }
     lazy: true
@@ -1069,7 +1075,7 @@ services:
   Drupal\Core\Routing\RouteBuilderInterface: '@router.builder'
   menu.rebuild_subscriber:
     class: Drupal\Core\EventSubscriber\MenuRouterRebuildSubscriber
-    arguments: ['@lock', '@plugin.manager.menu.link', '@database', '@database.replica_kill_switch']
+    arguments: ['@lock', '@plugin.manager.menu.link', '@database', '@database.replica_kill_switch', '@logger.channel.menu']
     tags:
       - { name: event_subscriber }
   path.matcher:
diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index 332f40e1bb2c..fdbe3f238cf4 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -124,8 +124,14 @@ function t($string, array $args = [], array $options = []) {
  *   A link to associate with the message.
  *
  * @see \Drupal\Core\Utility\Error::decodeException()
+ *
+ * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use
+ *   Use \Drupal\Core\Utility\Error::logException() instead.
+ *
+ * @see https://www.drupal.org/node/2932520
  */
 function watchdog_exception($type, Exception $exception, $message = NULL, $variables = [], $severity = RfcLogLevel::ERROR, $link = NULL) {
+  @trigger_error('watchdog_exception() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use \Drupal\Core\Utility\Error::logException() instead. See https://www.drupal.org/node/2932520', E_USER_DEPRECATED);
 
   // Use a default value if $message is not set.
   if (empty($message)) {
diff --git a/core/includes/update.inc b/core/includes/update.inc
index dac375df909f..2549249253f1 100644
--- a/core/includes/update.inc
+++ b/core/includes/update.inc
@@ -175,9 +175,9 @@ function update_do_one($module, $number, $dependency_map, &$context) {
     // return the message for printing.
     // @see https://www.drupal.org/node/2564311
     catch (Exception $e) {
-      watchdog_exception('update', $e);
-
       $variables = Error::decodeException($e);
+      \Drupal::logger('update')->error(Error::DEFAULT_ERROR_MESSAGE, $variables);
+
       unset($variables['backtrace'], $variables['exception'], $variables['severity_level']);
       $ret['#abort'] = ['success' => FALSE, 'query' => t(Error::DEFAULT_ERROR_MESSAGE, $variables)];
     }
@@ -244,9 +244,8 @@ function update_invoke_post_update($function, &$context) {
     // for printing.
     // @see https://www.drupal.org/node/2564311
     catch (Exception $e) {
-      watchdog_exception('update', $e);
-
       $variables = Error::decodeException($e);
+      \Drupal::logger('update')->error(Error::DEFAULT_ERROR_MESSAGE, $variables);
       unset($variables['backtrace'], $variables['exception'], $variables['severity_level']);
       $ret['#abort'] = [
         'success' => FALSE,
diff --git a/core/lib/Drupal/Core/Cron.php b/core/lib/Drupal/Core/Cron.php
index e48e7ef6a79d..146394341546 100644
--- a/core/lib/Drupal/Core/Cron.php
+++ b/core/lib/Drupal/Core/Cron.php
@@ -7,17 +7,18 @@
 use Drupal\Component\Utility\Timer;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Lock\LockBackendInterface;
-use Drupal\Core\Queue\QueueFactory;
 use Drupal\Core\Queue\DelayableQueueInterface;
+use Drupal\Core\Queue\DelayedRequeueException;
+use Drupal\Core\Queue\QueueFactory;
 use Drupal\Core\Queue\QueueInterface;
 use Drupal\Core\Queue\QueueWorkerInterface;
 use Drupal\Core\Queue\QueueWorkerManagerInterface;
-use Drupal\Core\Queue\DelayedRequeueException;
 use Drupal\Core\Queue\RequeueException;
 use Drupal\Core\Queue\SuspendQueueException;
 use Drupal\Core\Session\AccountSwitcherInterface;
 use Drupal\Core\Session\AnonymousUserSession;
 use Drupal\Core\State\StateInterface;
+use Drupal\Core\Utility\Error;
 use Psr\Log\LoggerInterface;
 use Psr\Log\NullLogger;
 
@@ -298,7 +299,7 @@ protected function processQueue(QueueInterface $queue, QueueWorkerInterface $wor
       catch (\Exception $e) {
         // In case of any other kind of exception, log it and leave the item
         // in the queue to be processed again later.
-        watchdog_exception('cron', $e);
+        Error::logException($this->logger, $e);
       }
     }
   }
@@ -334,7 +335,7 @@ protected function invokeCronHandlers() {
         $hook();
       }
       catch (\Exception $e) {
-        watchdog_exception('cron', $e);
+        Error::logException($this->logger, $e);
       }
 
       Timer::stop('cron_' . $module);
diff --git a/core/lib/Drupal/Core/Database/Transaction.php b/core/lib/Drupal/Core/Database/Transaction.php
index 8aed8c8ae672..76d5fc8f5fac 100644
--- a/core/lib/Drupal/Core/Database/Transaction.php
+++ b/core/lib/Drupal/Core/Database/Transaction.php
@@ -84,12 +84,10 @@ public function name() {
    *
    * This is just a wrapper method to rollback whatever transaction stack we are
    * currently in, which is managed by the connection object itself. Note that
-   * logging (preferable with watchdog_exception()) needs to happen after a
-   * transaction has been rolled back or the log messages will be rolled back
-   * too.
+   * logging needs to happen after a transaction has been rolled back or the log
+   * messages will be rolled back too.
    *
    * @see \Drupal\Core\Database\Connection::rollBack()
-   * @see watchdog_exception()
    */
   public function rollBack() {
     $this->rolledBack = TRUE;
diff --git a/core/lib/Drupal/Core/Database/database.api.php b/core/lib/Drupal/Core/Database/database.api.php
index e4e5a0b9be08..50e4bb506073 100644
--- a/core/lib/Drupal/Core/Database/database.api.php
+++ b/core/lib/Drupal/Core/Database/database.api.php
@@ -201,8 +201,8 @@
  *       $transaction->rollBack();
  *     }
  *
- *     // Log the exception to watchdog.
- *     watchdog_exception('type', $e);
+ *     // Log the exception.
+ *     Error::logException(\Drupal::logger('type'), $e);
  *   }
  *
  *   // $transaction goes out of scope here. Unless the transaction was rolled
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
index 01bf60ecd2bf..c65c623fef01 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
@@ -14,16 +14,17 @@
 use Drupal\Core\Entity\EntityBundleListenerInterface;
 use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Entity\EntityStorageException;
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Entity\Query\QueryInterface;
 use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Utility\Error;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -758,7 +759,7 @@ public function delete(array $entities) {
       if (isset($transaction)) {
         $transaction->rollBack();
       }
-      watchdog_exception($this->entityTypeId, $e);
+      Error::logException(\Drupal::logger($this->entityTypeId), $e);
       throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
     }
   }
@@ -812,7 +813,7 @@ public function save(EntityInterface $entity) {
       if (isset($transaction)) {
         $transaction->rollBack();
       }
-      watchdog_exception($this->entityTypeId, $e);
+      Error::logException(\Drupal::logger($this->entityTypeId), $e);
       throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
     }
   }
@@ -861,7 +862,7 @@ public function restore(EntityInterface $entity) {
       if (isset($transaction)) {
         $transaction->rollBack();
       }
-      watchdog_exception($this->entityTypeId, $e);
+      Error::logException(\Drupal::logger($this->entityTypeId), $e);
       throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
     }
   }
diff --git a/core/lib/Drupal/Core/EventSubscriber/MenuRouterRebuildSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/MenuRouterRebuildSubscriber.php
index 7d0d64c0ad30..a3aa52e863ef 100644
--- a/core/lib/Drupal/Core/EventSubscriber/MenuRouterRebuildSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/MenuRouterRebuildSubscriber.php
@@ -3,11 +3,13 @@
 namespace Drupal\Core\EventSubscriber;
 
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\Database\Connection;
 use Drupal\Core\Database\ReplicaKillSwitch;
 use Drupal\Core\Lock\LockBackendInterface;
 use Drupal\Core\Menu\MenuLinkManagerInterface;
 use Drupal\Core\Routing\RoutingEvents;
-use Drupal\Core\Database\Connection;
+use Drupal\Core\Utility\Error;
+use Psr\Log\LoggerInterface;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
@@ -52,12 +54,18 @@ class MenuRouterRebuildSubscriber implements EventSubscriberInterface {
    *   The database connection.
    * @param \Drupal\Core\Database\ReplicaKillSwitch $replica_kill_switch
    *   The replica kill switch.
+   * @param \Psr\Log\LoggerInterface|null $logger
+   *   The logger.
    */
-  public function __construct(LockBackendInterface $lock, MenuLinkManagerInterface $menu_link_manager, Connection $connection, ReplicaKillSwitch $replica_kill_switch) {
+  public function __construct(LockBackendInterface $lock, MenuLinkManagerInterface $menu_link_manager, Connection $connection, ReplicaKillSwitch $replica_kill_switch, protected ?LoggerInterface $logger = NULL) {
     $this->lock = $lock;
     $this->menuLinkManager = $menu_link_manager;
     $this->connection = $connection;
     $this->replicaKillSwitch = $replica_kill_switch;
+    if ($this->logger === NULL) {
+      @trigger_error('Calling ' . __METHOD__ . '() without the $logger argument is deprecated in drupal:10.1.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/2932520', E_USER_DEPRECATED);
+      $this->logger = \Drupal::service('logger.channel.menu');
+    }
   }
 
   /**
@@ -87,7 +95,7 @@ protected function menuLinksRebuild() {
         if (isset($transaction)) {
           $transaction->rollBack();
         }
-        watchdog_exception('menu', $e);
+        Error::logException($this->logger, $e);
       }
 
       $this->lock->release(__FUNCTION__);
diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
index a224491f6b02..7e53d711d59e 100644
--- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php
+++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
@@ -12,6 +12,8 @@
 use Drupal\Core\Installer\InstallerKernel;
 use Drupal\Core\Serialization\Yaml;
 use Drupal\Core\Update\UpdateHookRegistry;
+use Drupal\Core\Utility\Error;
+use Psr\Log\LoggerInterface;
 
 /**
  * Default implementation of the module installer.
@@ -81,16 +83,22 @@ class ModuleInstaller implements ModuleInstallerInterface {
    *   The database connection.
    * @param \Drupal\Core\Update\UpdateHookRegistry $update_registry
    *   The update registry service.
+   * @param \Psr\Log\LoggerInterface|null $logger
+   *   The logger.
    *
    * @see \Drupal\Core\DrupalKernel
    * @see \Drupal\Core\CoreServiceProvider
    */
-  public function __construct($root, ModuleHandlerInterface $module_handler, DrupalKernelInterface $kernel, Connection $connection, UpdateHookRegistry $update_registry) {
+  public function __construct($root, ModuleHandlerInterface $module_handler, DrupalKernelInterface $kernel, Connection $connection, UpdateHookRegistry $update_registry, protected ?LoggerInterface $logger = NULL) {
     $this->root = $root;
     $this->moduleHandler = $module_handler;
     $this->kernel = $kernel;
     $this->connection = $connection;
     $this->updateRegistry = $update_registry;
+    if ($this->logger === NULL) {
+      @trigger_error('Calling ' . __METHOD__ . ' without the $logger argument is deprecated in drupal:10.1.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/2932520', E_USER_DEPRECATED);
+      $this->logger = \Drupal::service('logger.channel.system');
+    }
   }
 
   /**
@@ -302,7 +310,7 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
                   $update_manager->installFieldStorageDefinition($storage_definition->getName(), $entity_type->id(), $module, $storage_definition);
                 }
                 catch (EntityStorageException $e) {
-                  watchdog_exception('system', $e, 'An error occurred while notifying the creation of the @name field storage definition: "@message" in %function (line %line of %file).', ['@name' => $storage_definition->getName(), '@message' => $e->getMessage()]);
+                  Error::logException($this->logger, $e, 'An error occurred while notifying the creation of the @name field storage definition: "@message" in %function (line %line of %file).', ['@name' => $storage_definition->getName()]);
                 }
               }
             }
diff --git a/core/lib/Drupal/Core/Routing/MatcherDumper.php b/core/lib/Drupal/Core/Routing/MatcherDumper.php
index 2757012f254b..cbd3aac0d048 100644
--- a/core/lib/Drupal/Core/Routing/MatcherDumper.php
+++ b/core/lib/Drupal/Core/Routing/MatcherDumper.php
@@ -4,6 +4,8 @@
 
 use Drupal\Core\Database\DatabaseException;
 use Drupal\Core\State\StateInterface;
+use Drupal\Core\Utility\Error;
+use Psr\Log\LoggerInterface;
 use Symfony\Component\Routing\RouteCollection;
 
 use Drupal\Core\Database\Connection;
@@ -43,6 +45,13 @@ class MatcherDumper implements MatcherDumperInterface {
    */
   protected $tableName;
 
+  /**
+   * The logger.
+   *
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected LoggerInterface $logger;
+
   /**
    * Construct the MatcherDumper.
    *
@@ -51,14 +60,25 @@ class MatcherDumper implements MatcherDumperInterface {
    *   information.
    * @param \Drupal\Core\State\StateInterface $state
    *   The state.
+   * @param \Psr\Log\LoggerInterface|null $logger
+   *   The logger.
    * @param string $table
    *   (optional) The table to store the route info in. Defaults to 'router'.
    */
-  public function __construct(Connection $connection, StateInterface $state, $table = 'router') {
+  public function __construct(Connection $connection, StateInterface $state, LoggerInterface|string|null $logger = NULL, $table = 'router') {
     $this->connection = $connection;
     $this->state = $state;
-
-    $this->tableName = $table;
+    if (is_string($logger) || is_null($logger)) {
+      @trigger_error('Calling ' . __METHOD__ . '() without the $logger argument is deprecated in drupal:10.1.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/2932520', E_USER_DEPRECATED);
+      $this->logger = \Drupal::service('logger.channel.router');
+      $this->tableName = $logger;
+    }
+    else {
+      $this->logger = $logger;
+    }
+    if (is_null($this->tableName)) {
+      $this->tableName = $table;
+    }
   }
 
   /**
@@ -152,7 +172,7 @@ public function dump(array $options = []): string {
       if (isset($transaction)) {
         $transaction->rollBack();
       }
-      watchdog_exception('Routing', $e);
+      Error::logException($this->logger, $e);
       throw $e;
     }
     // Sort the masks so they are in order of descending fit.
diff --git a/core/lib/Drupal/Core/Utility/Error.php b/core/lib/Drupal/Core/Utility/Error.php
index d5bb777f06f0..ab8c3bfa90bd 100644
--- a/core/lib/Drupal/Core/Utility/Error.php
+++ b/core/lib/Drupal/Core/Utility/Error.php
@@ -7,6 +7,8 @@
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Database\Database;
 use Drupal\Core\Database\DatabaseExceptionWrapper;
+use Psr\Log\LoggerInterface;
+use Psr\Log\LogLevel;
 
 /**
  * Drupal error utility class.
@@ -75,6 +77,24 @@ public static function decodeException($exception) {
     ];
   }
 
+  /**
+   * Log a formatted exception message to the provided logger.
+   *
+   * @param \Psr\Log\LoggerInterface $logger
+   *   The logger.
+   * @param \Throwable $exception
+   *   The exception.
+   * @param string $message
+   *   (optional) The message.
+   * @param array $additional_variables
+   *   (optional) Any additional variables.
+   * @param string $level
+   *   The PSR log level. Must be valid constant in \Psr\Log\LogLevel.
+   */
+  public static function logException(LoggerInterface $logger, \Throwable $exception, string $message = Error::DEFAULT_ERROR_MESSAGE, array $additional_variables = [], string $level = LogLevel::ERROR): void {
+    $logger->log($level, $message, static::decodeException($exception) + $additional_variables);
+  }
+
   /**
    * Renders an exception error message without further exceptions.
    *
diff --git a/core/modules/sdc/sdc.services.yml b/core/modules/sdc/sdc.services.yml
index e9ad6726b9f9..519acef6100d 100644
--- a/core/modules/sdc/sdc.services.yml
+++ b/core/modules/sdc/sdc.services.yml
@@ -8,6 +8,7 @@ services:
   Drupal\sdc\Twig\TwigComponentLoader:
     arguments:
       - '@plugin.manager.sdc'
+      - '@logger.channel.default'
     tags:
       - { name: twig.loader, priority: 5 }
 
diff --git a/core/modules/sdc/src/Twig/TwigComponentLoader.php b/core/modules/sdc/src/Twig/TwigComponentLoader.php
index f530dbc714e8..baa069a7584a 100644
--- a/core/modules/sdc/src/Twig/TwigComponentLoader.php
+++ b/core/modules/sdc/src/Twig/TwigComponentLoader.php
@@ -2,9 +2,11 @@
 
 namespace Drupal\sdc\Twig;
 
+use Drupal\Core\Utility\Error;
 use Drupal\sdc\ComponentPluginManager;
 use Drupal\sdc\Exception\ComponentNotFoundException;
 use Drupal\Component\Discovery\YamlDirectoryDiscovery;
+use Psr\Log\LoggerInterface;
 use Twig\Error\LoaderError;
 use Twig\Loader\LoaderInterface;
 use Twig\Source;
@@ -21,8 +23,13 @@ final class TwigComponentLoader implements LoaderInterface {
    *
    * @param \Drupal\sdc\ComponentPluginManager $pluginManager
    *   The plugin manager.
+   * @param \Psr\Log\LoggerInterface $logger
+   *   The logger.
    */
-  public function __construct(protected ComponentPluginManager $pluginManager) {}
+  public function __construct(
+    protected ComponentPluginManager $pluginManager,
+    protected LoggerInterface $logger,
+  ) {}
 
   /**
    * Finds a template in the file system based on the template name.
@@ -69,7 +76,7 @@ public function exists($name): bool {
       return TRUE;
     }
     catch (ComponentNotFoundException $e) {
-      watchdog_exception('sdc', $e);
+      Error::logException($this->logger, $e);
       return FALSE;
     }
   }
diff --git a/core/modules/system/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php b/core/modules/system/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
index b8b0a5323692..3b29384231a6 100644
--- a/core/modules/system/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
+++ b/core/modules/system/src/SecurityAdvisories/SecurityAdvisoriesFetcher.php
@@ -9,6 +9,7 @@
 use Drupal\Core\Extension\ThemeExtensionList;
 use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface;
 use Drupal\Core\Site\Settings;
+use Drupal\Core\Utility\Error;
 use Drupal\Core\Utility\ProjectInfo;
 use Drupal\Core\Extension\ExtensionVersion;
 use GuzzleHttp\ClientInterface;
@@ -152,7 +153,7 @@ public function getSecurityAdvisories(bool $allow_outgoing_request = TRUE, int $
         // Ignore items in the feed that are in an invalid format. Although
         // this is highly unlikely we should still display the items that are
         // in the correct format.
-        watchdog_exception('system', $unexpected_value_exception, 'Invalid security advisory format: ' . Json::encode($advisory_data));
+        Error::logException($this->logger, $unexpected_value_exception, 'Invalid security advisory format: @advisory', ['@advisory' => Json::encode($advisory_data)]);
         continue;
       }
 
@@ -321,7 +322,7 @@ protected function doRequest(int $timeout): string {
         $response = $this->httpClient->get('https://updates.drupal.org/psa.json', $options);
       }
       catch (TransferException $exception) {
-        watchdog_exception('system', $exception);
+        Error::logException($this->logger, $exception);
         $response = $this->httpClient->get('http://updates.drupal.org/psa.json', $options);
       }
     }
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index b7a7e10dad16..c6513010faa0 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -25,6 +25,7 @@
 use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Url;
+use Drupal\Core\Utility\Error;
 use GuzzleHttp\Exception\TransferException;
 use Symfony\Component\HttpFoundation\Request;
 
@@ -1652,7 +1653,7 @@ function _system_advisories_requirements(array &$requirements): void {
     $requirements['system_advisories']['title'] = t('Critical security announcements');
     $requirements['system_advisories']['severity'] = REQUIREMENT_WARNING;
     $requirements['system_advisories']['description'] = ['#theme' => 'system_security_advisories_fetch_error_message'];
-    watchdog_exception('system', $exception, 'Failed to retrieve security advisory data.');
+    Error::logException(\Drupal::logger('system'), $exception, 'Failed to retrieve security advisory data.');
     return;
   }
 
diff --git a/core/modules/system/tests/src/Kernel/SecurityAdvisories/SecurityAdvisoriesFetcherTest.php b/core/modules/system/tests/src/Kernel/SecurityAdvisories/SecurityAdvisoriesFetcherTest.php
index da258d42712e..c6cebd1201a6 100644
--- a/core/modules/system/tests/src/Kernel/SecurityAdvisories/SecurityAdvisoriesFetcherTest.php
+++ b/core/modules/system/tests/src/Kernel/SecurityAdvisories/SecurityAdvisoriesFetcherTest.php
@@ -26,11 +26,11 @@ class SecurityAdvisoriesFetcherTest extends KernelTestBase implements LoggerInte
   use RfcLoggerTrait;
 
   /**
-   * The log messages from watchdog_exception.
+   * The error messages.
    *
    * @var string[]
    */
-  protected $watchdogExceptionMessages = [];
+  protected $errorMessages = [];
 
   /**
    * The log error log messages.
@@ -658,7 +658,7 @@ public function testHttpFallback(): void {
     $this->assertCount(1, $advisories);
     $this->assertSame('http://example.com', $advisories[0]->getUrl());
     $this->assertSame('SA title', $advisories[0]->getTitle());
-    $this->assertSame(["Server error: `GET https://updates.drupal.org/psa.json` resulted in a `500 Internal Server Error` response:\nHTTPS failed\n"], $this->watchdogExceptionMessages);
+    $this->assertSame(["Server error: `GET https://updates.drupal.org/psa.json` resulted in a `500 Internal Server Error` response:\nHTTPS failed\n"], $this->errorMessages);
   }
 
   /**
@@ -745,7 +745,7 @@ protected function assertServiceAdvisoryLoggedErrors(array $expected_messages):
    */
   public function log($level, $message, array $context = []): void {
     if (isset($context['@message'])) {
-      $this->watchdogExceptionMessages[] = $context['@message'];
+      $this->errorMessages[] = $context['@message'];
     }
     if ($level === RfcLogLevel::ERROR) {
       $this->logErrorMessages[] = $message;
diff --git a/core/modules/update/src/ProjectSecurityData.php b/core/modules/update/src/ProjectSecurityData.php
index 6c927c5e0f3f..daa210568981 100644
--- a/core/modules/update/src/ProjectSecurityData.php
+++ b/core/modules/update/src/ProjectSecurityData.php
@@ -3,6 +3,7 @@
 namespace Drupal\update;
 
 use Drupal\Core\Extension\ExtensionVersion;
+use Drupal\Core\Utility\Error;
 
 /**
  * Calculates a project's security coverage information.
@@ -224,12 +225,7 @@ private function getAdditionalSecurityCoveredMinors($security_covered_version) {
         // Ignore releases that are in an invalid format. Although this is
         // highly unlikely we should still process releases in the correct
         // format.
-        watchdog_exception(
-          'update',
-          $exception,
-          'Invalid project format: @release',
-          ['@release' => print_r($release_info, TRUE)]
-        );
+        Error::logException(\Drupal::logger('update'), $exception, 'Invalid project format: @release', ['@release' => print_r($release_info, TRUE)]);
         continue;
       }
       $release_version = ExtensionVersion::createFromVersionString($release->getVersion());
diff --git a/core/modules/update/src/UpdateFetcher.php b/core/modules/update/src/UpdateFetcher.php
index 6d7c71cc04dc..9a6704ac68f0 100644
--- a/core/modules/update/src/UpdateFetcher.php
+++ b/core/modules/update/src/UpdateFetcher.php
@@ -5,8 +5,10 @@
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
 use Drupal\Core\Site\Settings;
+use Drupal\Core\Utility\Error;
 use GuzzleHttp\ClientInterface;
 use GuzzleHttp\Exception\TransferException;
+use Psr\Log\LoggerInterface;
 
 /**
  * Fetches project information from remote locations.
@@ -57,12 +59,18 @@ class UpdateFetcher implements UpdateFetcherInterface {
    *   A Guzzle client object.
    * @param \Drupal\Core\Site\Settings $settings
    *   The settings instance.
+   * @param \Psr\Log\LoggerInterface|null $logger
+   *   The logger.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, ClientInterface $http_client, Settings $settings) {
+  public function __construct(ConfigFactoryInterface $config_factory, ClientInterface $http_client, Settings $settings, protected ?LoggerInterface $logger = NULL) {
     $this->fetchUrl = $config_factory->get('update.settings')->get('fetch.url');
     $this->httpClient = $http_client;
     $this->updateSettings = $config_factory->get('update.settings');
     $this->withHttpFallback = $settings->get('update_fetch_with_http_fallback', FALSE);
+    if ($this->logger === NULL) {
+      @trigger_error('Calling ' . __METHOD__ . '() without the $logger argument is deprecated in drupal:10.1.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/2932520', E_USER_DEPRECATED);
+      $this->logger = \Drupal::service('logger.channel.update');
+    }
   }
 
   /**
@@ -97,7 +105,7 @@ protected function doRequest(string $url, array $options, bool $with_http_fallba
         ->getBody();
     }
     catch (TransferException $exception) {
-      watchdog_exception('update', $exception);
+      Error::logException($this->logger, $exception);
       if ($with_http_fallback && !str_contains($url, "http://")) {
         $url = str_replace('https://', 'http://', $url);
         return $this->doRequest($url, $options, FALSE);
diff --git a/core/modules/update/tests/src/Unit/UpdateFetcherTest.php b/core/modules/update/tests/src/Unit/UpdateFetcherTest.php
index 325d687dee9c..cde0a60c3ded 100644
--- a/core/modules/update/tests/src/Unit/UpdateFetcherTest.php
+++ b/core/modules/update/tests/src/Unit/UpdateFetcherTest.php
@@ -2,8 +2,7 @@
 
 namespace Drupal\Tests\update\Unit;
 
-use Drupal\Core\Logger\LoggerChannelFactory;
-use Drupal\Core\Logger\RfcLoggerTrait;
+use ColinODell\PsrTestLogger\TestLogger;
 use Drupal\Core\Site\Settings;
 use Drupal\Tests\UnitTestCase;
 use Drupal\update\UpdateFetcher;
@@ -21,8 +20,7 @@
  *
  * @group update
  */
-class UpdateFetcherTest extends UnitTestCase implements LoggerInterface {
-  use RfcLoggerTrait;
+class UpdateFetcherTest extends UnitTestCase {
 
   /**
    * The update fetcher to use.
@@ -60,9 +58,11 @@ class UpdateFetcherTest extends UnitTestCase implements LoggerInterface {
   protected $testProject;
 
   /**
-   * @var array
+   * The logger.
+   *
+   * @var \Psr\Log\LoggerInterface
    */
-  protected $logMessages = [];
+  protected LoggerInterface $logger;
 
   /**
    * {@inheritdoc}
@@ -72,7 +72,8 @@ protected function setUp(): void {
     $this->mockConfigFactory = $this->getConfigFactoryStub(['update.settings' => ['fetch_url' => 'http://www.example.com']]);
     $this->mockHttpClient = $this->createMock('\GuzzleHttp\ClientInterface');
     $settings = new Settings([]);
-    $this->updateFetcher = new UpdateFetcher($this->mockConfigFactory, $this->mockHttpClient, $settings);
+    $this->logger = new TestLogger();
+    $this->updateFetcher = new UpdateFetcher($this->mockConfigFactory, $this->mockHttpClient, $settings, $this->logger);
     $this->testProject = [
       'name' => 'update_test',
       'project_type' => '',
@@ -82,17 +83,6 @@ protected function setUp(): void {
       ],
       'includes' => ['module1' => 'Module 1', 'module2' => 'Module 2'],
     ];
-
-    // Set up logger factory so that watchdog_exception() does not break and
-    // register this class as the logger so we can test messages.
-    $container = $this->createMock('Symfony\Component\DependencyInjection\ContainerInterface');
-    $logger_factory = new LoggerChannelFactory();
-    $logger_factory->addLogger($this);
-    $container->expects($this->any())
-      ->method('get')
-      ->with('logger.factory')
-      ->willReturn($logger_factory);
-    \Drupal::setContainer($container);
   }
 
   /**
@@ -113,7 +103,7 @@ protected function setUp(): void {
   public function testUpdateBuildFetchUrl(array $project, $site_key, $expected) {
     $url = $this->updateFetcher->buildFetchUrl($project, $site_key);
     $this->assertEquals($url, $expected);
-    $this->assertSame([], $this->logMessages);
+    $this->assertFalse($this->logger->hasErrorRecords());
   }
 
   /**
@@ -190,7 +180,7 @@ public function testUpdateFetcherNoFallback() {
     $this->mockClient(
       new Response('500', [], 'HTTPS failed'),
     );
-    $update_fetcher = new UpdateFetcher($this->mockConfigFactory, $this->mockHttpClient, $settings);
+    $update_fetcher = new UpdateFetcher($this->mockConfigFactory, $this->mockHttpClient, $settings, $this->logger);
 
     $data = $update_fetcher->fetchProjectData($this->testProject, '');
     // There should only be one request / response pair.
@@ -203,7 +193,10 @@ public function testUpdateFetcherNoFallback() {
     $response = $this->history[0]['response'];
     $this->assertEquals(500, $response->getStatusCode());
     $this->assertEmpty($data);
-    $this->assertSame(["Server error: `GET https://www.example.com/update_test/current` resulted in a `500 Internal Server Error` response:\nHTTPS failed\n"], $this->logMessages);
+
+    $this->assertTrue($this->logger->hasErrorThatPasses(function (array $record) {
+      return $record['context']['@message'] === "Server error: `GET https://www.example.com/update_test/current` resulted in a `500 Internal Server Error` response:\nHTTPS failed\n";
+    }));
   }
 
   /**
@@ -216,7 +209,7 @@ public function testUpdateFetcherHttpFallback() {
       new Response('500', [], 'HTTPS failed'),
       new Response('200', [], 'HTTP worked'),
     );
-    $update_fetcher = new UpdateFetcher($this->mockConfigFactory, $this->mockHttpClient, $settings);
+    $update_fetcher = new UpdateFetcher($this->mockConfigFactory, $this->mockHttpClient, $settings, $this->logger);
 
     $data = $update_fetcher->fetchProjectData($this->testProject, '');
 
@@ -237,14 +230,9 @@ public function testUpdateFetcherHttpFallback() {
     // Although this is a bogus mocked response, it's what fetchProjectData()
     // should return in this case.
     $this->assertEquals('HTTP worked', $data);
-    $this->assertSame(["Server error: `GET https://www.example.com/update_test/current` resulted in a `500 Internal Server Error` response:\nHTTPS failed\n"], $this->logMessages);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function log($level, string|\Stringable $message, array $context = []): void {
-    $this->logMessages[] = $context['@message'];
+    $this->assertTrue($this->logger->hasErrorThatPasses(function (array $record) {
+      return $record['context']['@message'] === "Server error: `GET https://www.example.com/update_test/current` resulted in a `500 Internal Server Error` response:\nHTTPS failed\n";
+    }));
   }
 
 }
diff --git a/core/modules/update/update.compare.inc b/core/modules/update/update.compare.inc
index 7b8fc6899cfd..d1b5a8e73de9 100644
--- a/core/modules/update/update.compare.inc
+++ b/core/modules/update/update.compare.inc
@@ -6,6 +6,7 @@
  */
 
 use Drupal\Core\Extension\ExtensionVersion;
+use Drupal\Core\Utility\Error;
 use Drupal\update\ProjectRelease;
 use Drupal\update\UpdateFetcherInterface;
 use Drupal\update\UpdateManagerInterface;
@@ -363,12 +364,7 @@ function update_calculate_project_update_status(&$project_data, $available) {
     catch (UnexpectedValueException $exception) {
       // Ignore releases that are in an invalid format. Although this is highly
       // unlikely we should still process releases in the correct format.
-      watchdog_exception(
-        'update',
-        $exception,
-        'Invalid project format: @release',
-        ['@release' => print_r($release_info, TRUE)]
-      );
+      Error::logException(\Drupal::logger('update'), $exception, 'Invalid project format: @release', ['@release' => print_r($release_info, TRUE)]);
       continue;
     }
 
diff --git a/core/modules/update/update.services.yml b/core/modules/update/update.services.yml
index d243fedcdbeb..545612dcb35b 100644
--- a/core/modules/update/update.services.yml
+++ b/core/modules/update/update.services.yml
@@ -12,7 +12,10 @@ services:
     arguments: ['@config.factory', '@queue', '@update.fetcher', '@state', '@private_key', '@keyvalue', '@keyvalue.expirable']
   update.fetcher:
     class: Drupal\update\UpdateFetcher
-    arguments: ['@config.factory', '@http_client', '@settings']
+    arguments: ['@config.factory', '@http_client', '@settings', '@logger.channel.update']
   update.root:
     class: Drupal\update\UpdateRoot
     arguments: ['@kernel', '@request_stack']
+  logger.channel.update:
+    parent: logger.channel_base
+    arguments: [ 'update' ]
diff --git a/core/modules/workspaces/src/WorkspaceAssociation.php b/core/modules/workspaces/src/WorkspaceAssociation.php
index aa6f8078a24a..c9bcee9219ec 100644
--- a/core/modules/workspaces/src/WorkspaceAssociation.php
+++ b/core/modules/workspaces/src/WorkspaceAssociation.php
@@ -6,8 +6,10 @@
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Entity\RevisionableInterface;
 use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
+use Drupal\Core\Utility\Error;
 use Drupal\workspaces\Event\WorkspacePostPublishEvent;
 use Drupal\workspaces\Event\WorkspacePublishEvent;
+use Psr\Log\LoggerInterface;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
@@ -50,11 +52,17 @@ class WorkspaceAssociation implements WorkspaceAssociationInterface, EventSubscr
    *   The entity type manager for querying revisions.
    * @param \Drupal\workspaces\WorkspaceRepositoryInterface $workspace_repository
    *   The Workspace repository service.
+   * @param \Psr\Log\LoggerInterface|null $logger
+   *   The logger.
    */
-  public function __construct(Connection $connection, EntityTypeManagerInterface $entity_type_manager, WorkspaceRepositoryInterface $workspace_repository) {
+  public function __construct(Connection $connection, EntityTypeManagerInterface $entity_type_manager, WorkspaceRepositoryInterface $workspace_repository, protected ?LoggerInterface $logger = NULL) {
     $this->database = $connection;
     $this->entityTypeManager = $entity_type_manager;
     $this->workspaceRepository = $workspace_repository;
+    if ($this->logger === NULL) {
+      @trigger_error('Calling ' . __METHOD__ . '() without the $logger argument is deprecated in drupal:10.1.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/2932520', E_USER_DEPRECATED);
+      $this->logger = \Drupal::service('logger.channel.workspaces');
+    }
   }
 
   /**
@@ -116,7 +124,7 @@ public function trackEntity(RevisionableInterface $entity, WorkspaceInterface $w
       if (isset($transaction)) {
         $transaction->rollBack();
       }
-      watchdog_exception('workspaces', $e);
+      Error::logException($this->logger, $e);
       throw $e;
     }
   }
diff --git a/core/modules/workspaces/src/WorkspaceMerger.php b/core/modules/workspaces/src/WorkspaceMerger.php
index afdabc755ada..2500ea157748 100644
--- a/core/modules/workspaces/src/WorkspaceMerger.php
+++ b/core/modules/workspaces/src/WorkspaceMerger.php
@@ -5,6 +5,8 @@
 use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Utility\Error;
+use Psr\Log\LoggerInterface;
 
 /**
  * Default implementation of the workspace merger.
@@ -70,14 +72,20 @@ class WorkspaceMerger implements WorkspaceMergerInterface {
    *   The source workspace.
    * @param \Drupal\workspaces\WorkspaceInterface $target
    *   The target workspace.
+   * @param \Psr\Log\LoggerInterface|null $logger
+   *   The logger.
    */
-  public function __construct(EntityTypeManagerInterface $entity_type_manager, Connection $database, WorkspaceAssociationInterface $workspace_association, CacheTagsInvalidatorInterface $cache_tags_invalidator, WorkspaceInterface $source, WorkspaceInterface $target) {
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, Connection $database, WorkspaceAssociationInterface $workspace_association, CacheTagsInvalidatorInterface $cache_tags_invalidator, WorkspaceInterface $source, WorkspaceInterface $target, protected ?LoggerInterface $logger = NULL) {
     $this->entityTypeManager = $entity_type_manager;
     $this->database = $database;
     $this->workspaceAssociation = $workspace_association;
     $this->cacheTagsInvalidator = $cache_tags_invalidator;
     $this->sourceWorkspace = $source;
     $this->targetWorkspace = $target;
+    if ($this->logger === NULL) {
+      @trigger_error('Calling ' . __METHOD__ . '() without the $logger argument is deprecated in drupal:10.1.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/2932520', E_USER_DEPRECATED);
+      $this->logger = \Drupal::service('logger.channel.workspaces');
+    }
   }
 
   /**
@@ -116,7 +124,7 @@ public function merge() {
       if (isset($transaction)) {
         $transaction->rollBack();
       }
-      watchdog_exception('workspaces', $e);
+      Error::logException($this->logger, $e);
       throw $e;
     }
   }
diff --git a/core/modules/workspaces/src/WorkspaceOperationFactory.php b/core/modules/workspaces/src/WorkspaceOperationFactory.php
index 4310243ff206..1f4474cd4e54 100644
--- a/core/modules/workspaces/src/WorkspaceOperationFactory.php
+++ b/core/modules/workspaces/src/WorkspaceOperationFactory.php
@@ -5,6 +5,7 @@
 use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Psr\Log\LoggerInterface;
 use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
 
 /**
@@ -74,8 +75,10 @@ class WorkspaceOperationFactory {
    *   The cache tags invalidator service.
    * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
    *   The event dispatcher.
+   * @param \Psr\Log\LoggerInterface|null $logger
+   *   The logger.
    */
-  public function __construct(EntityTypeManagerInterface $entity_type_manager, Connection $database, WorkspaceManagerInterface $workspace_manager, WorkspaceAssociationInterface $workspace_association, CacheTagsInvalidatorInterface $cache_tags_invalidator, EventDispatcherInterface $event_dispatcher = NULL) {
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, Connection $database, WorkspaceManagerInterface $workspace_manager, WorkspaceAssociationInterface $workspace_association, CacheTagsInvalidatorInterface $cache_tags_invalidator, EventDispatcherInterface $event_dispatcher = NULL, protected ?LoggerInterface $logger = NULL) {
     $this->entityTypeManager = $entity_type_manager;
     $this->database = $database;
     $this->workspaceManager = $workspace_manager;
@@ -86,6 +89,10 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Con
       $event_dispatcher = \Drupal::service('event_dispatcher');
     }
     $this->eventDispatcher = $event_dispatcher;
+    if ($this->logger === NULL) {
+      @trigger_error('Calling ' . __METHOD__ . '() without the $logger argument is deprecated in drupal:10.1.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/2932520', E_USER_DEPRECATED);
+      $this->logger = \Drupal::service('logger.channel.workspaces');
+    }
   }
 
   /**
@@ -98,7 +105,7 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Con
    *   A workspace publisher object.
    */
   public function getPublisher(WorkspaceInterface $source) {
-    return new WorkspacePublisher($this->entityTypeManager, $this->database, $this->workspaceManager, $this->workspaceAssociation, $this->eventDispatcher, $source);
+    return new WorkspacePublisher($this->entityTypeManager, $this->database, $this->workspaceManager, $this->workspaceAssociation, $this->eventDispatcher, $source, $this->logger);
   }
 
   /**
@@ -113,7 +120,7 @@ public function getPublisher(WorkspaceInterface $source) {
    *   A workspace merger object.
    */
   public function getMerger(WorkspaceInterface $source, WorkspaceInterface $target) {
-    return new WorkspaceMerger($this->entityTypeManager, $this->database, $this->workspaceAssociation, $this->cacheTagsInvalidator, $source, $target);
+    return new WorkspaceMerger($this->entityTypeManager, $this->database, $this->workspaceAssociation, $this->cacheTagsInvalidator, $source, $target, $this->logger);
   }
 
 }
diff --git a/core/modules/workspaces/src/WorkspacePublisher.php b/core/modules/workspaces/src/WorkspacePublisher.php
index a05b894318e4..79cd067f0e2a 100644
--- a/core/modules/workspaces/src/WorkspacePublisher.php
+++ b/core/modules/workspaces/src/WorkspacePublisher.php
@@ -5,8 +5,10 @@
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Utility\Error;
 use Drupal\workspaces\Event\WorkspacePostPublishEvent;
 use Drupal\workspaces\Event\WorkspacePrePublishEvent;
+use Psr\Log\LoggerInterface;
 
 /**
  * Default implementation of the workspace publisher.
@@ -74,8 +76,10 @@ class WorkspacePublisher implements WorkspacePublisherInterface {
    *   The event dispatcher.
    * @param \Drupal\workspaces\WorkspaceInterface $source
    *   The source workspace entity.
+   * @param \Psr\Log\LoggerInterface|null $logger
+   *   The logger.
    */
-  public function __construct(EntityTypeManagerInterface $entity_type_manager, Connection $database, WorkspaceManagerInterface $workspace_manager, WorkspaceAssociationInterface $workspace_association, $event_dispatcher, WorkspaceInterface $source = NULL) {
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, Connection $database, WorkspaceManagerInterface $workspace_manager, WorkspaceAssociationInterface $workspace_association, $event_dispatcher, WorkspaceInterface $source = NULL, protected ?LoggerInterface $logger = NULL) {
     $this->entityTypeManager = $entity_type_manager;
     $this->database = $database;
     $this->workspaceManager = $workspace_manager;
@@ -87,6 +91,10 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Con
     }
     $this->eventDispatcher = $event_dispatcher;
     $this->sourceWorkspace = $source;
+    if ($this->logger === NULL) {
+      @trigger_error('Calling ' . __METHOD__ . '() without the $logger argument is deprecated in drupal:10.1.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/2932520', E_USER_DEPRECATED);
+      $this->logger = \Drupal::service('logger.channel.workspaces');
+    }
   }
 
   /**
@@ -142,7 +150,7 @@ public function publish() {
       if (isset($transaction)) {
         $transaction->rollBack();
       }
-      watchdog_exception('workspaces', $e);
+      Error::logException($this->logger, $e);
       throw $e;
     }
 
diff --git a/core/modules/workspaces/workspaces.services.yml b/core/modules/workspaces/workspaces.services.yml
index 57b02cd82649..4aa7de957aa1 100644
--- a/core/modules/workspaces/workspaces.services.yml
+++ b/core/modules/workspaces/workspaces.services.yml
@@ -6,10 +6,10 @@ services:
       - { name: service_id_collector, tag: workspace_negotiator }
   workspaces.operation_factory:
     class: Drupal\workspaces\WorkspaceOperationFactory
-    arguments: ['@entity_type.manager', '@database', '@workspaces.manager', '@workspaces.association', '@cache_tags.invalidator', '@event_dispatcher']
+    arguments: ['@entity_type.manager', '@database', '@workspaces.manager', '@workspaces.association', '@cache_tags.invalidator', '@event_dispatcher', '@logger.channel.workspaces']
   workspaces.association:
     class: Drupal\workspaces\WorkspaceAssociation
-    arguments: ['@database', '@entity_type.manager', '@workspaces.repository']
+    arguments: ['@database', '@entity_type.manager', '@workspaces.repository', '@logger.channel.workspaces']
     tags:
       - { name: backend_overridable }
       - { name: event_subscriber }
diff --git a/core/tests/Drupal/KernelTests/Core/Bootstrap/LegacyBootstrapTest.php b/core/tests/Drupal/KernelTests/Core/Bootstrap/LegacyBootstrapTest.php
new file mode 100644
index 000000000000..a9f506bad28f
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Bootstrap/LegacyBootstrapTest.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Bootstrap;
+
+use ColinODell\PsrTestLogger\TestLogger;
+use Drupal\Core\Logger\RfcLogLevel;
+use Drupal\Core\Utility\Error;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests legacy bootstrap functions.
+ *
+ * @group Bootstrap
+ * @group legacy
+ */
+class LegacyBootstrapTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['system'];
+
+  /**
+   * Tests watchdog_exception() deprecation.
+   */
+  public function testWatchdogException(): void {
+    $logger = new TestLogger();
+    /** @var \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory */
+    $loggerFactory = \Drupal::service('logger.factory');
+    $loggerFactory->addLogger($logger);
+    $this->expectDeprecation('watchdog_exception() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use \Drupal\Core\Utility\Error::logException() instead. See https://www.drupal.org/node/2932520');
+    $e = new \RuntimeException("foo");
+    watchdog_exception('test', $e);
+    $this->assertTrue($logger->hasRecordThatContains(Error::DEFAULT_ERROR_MESSAGE, RfcLogLevel::ERROR));
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Routing/LegacyMatcherDumperTest.php b/core/tests/Drupal/KernelTests/Core/Routing/LegacyMatcherDumperTest.php
new file mode 100644
index 000000000000..e43bc5e436d4
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Routing/LegacyMatcherDumperTest.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Routing;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Routing\MatcherDumper;
+use Drupal\Core\State\State;
+use Drupal\KernelTests\KernelTestBase;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Tests deprecations in MatcherDumper.
+ *
+ * @group Routing
+ * @group legacy
+ * @coversDefaultClass \Drupal\Core\Routing\MatcherDumper
+ */
+class LegacyMatcherDumperTest extends KernelTestBase {
+
+  /**
+   * The connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected Connection $connection;
+
+  /**
+   * The state.
+   *
+   * @var \Drupal\Core\State\State
+   */
+  protected State $state;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp():void {
+    parent::setUp();
+    $this->connection = $this->createMock(Connection::class);
+    $this->state = $this->createMock(State::class);
+  }
+
+  /**
+   * Tests the constructor deprecations.
+   */
+  public function testConstructorDeprecationNoLogger() {
+    $this->expectDeprecation('Calling Drupal\Core\Routing\MatcherDumper::__construct() without the $logger argument is deprecated in drupal:10.1.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/2932520');
+    $dumper = new MatcherDumper($this->connection, $this->state);
+    $this->assertNotNull($dumper);
+  }
+
+  /**
+   * Tests the constructor deprecations.
+   */
+  public function testConstructorDeprecationWithLegacyTableNameParam() {
+    $this->expectDeprecation('Calling Drupal\Core\Routing\MatcherDumper::__construct() without the $logger argument is deprecated in drupal:10.1.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/2932520');
+    $dumper = new MatcherDumper($this->connection, $this->state, 'foo');
+    $this->assertNotNull($dumper);
+  }
+
+  /**
+   * Tests the constructor deprecations.
+   */
+  public function testConstructorDeprecationWithLogger() {
+    $logger = $this->createMock(LoggerInterface::class);
+    $dumper = new MatcherDumper($this->connection, $this->state, $logger);
+    $this->assertNotNull($dumper);
+
+    $logger = $this->createMock(LoggerInterface::class);
+    $dumper = new MatcherDumper($this->connection, $this->state, $logger, 'foo');
+    $this->assertNotNull($dumper);
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Routing/MatcherDumperTest.php b/core/tests/Drupal/KernelTests/Core/Routing/MatcherDumperTest.php
index 4efddf25c1a0..cb36c66f3b60 100644
--- a/core/tests/Drupal/KernelTests/Core/Routing/MatcherDumperTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Routing/MatcherDumperTest.php
@@ -2,15 +2,16 @@
 
 namespace Drupal\KernelTests\Core\Routing;
 
+use ColinODell\PsrTestLogger\TestLogger;
+use Drupal\Core\Database\Database;
 use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
+use Drupal\Core\Routing\MatcherDumper;
 use Drupal\Core\Routing\RouteCompiler;
 use Drupal\Core\State\State;
 use Drupal\KernelTests\KernelTestBase;
+use Drupal\Tests\Core\Routing\RoutingFixtures;
 use Symfony\Component\Routing\Route;
 use Symfony\Component\Routing\RouteCollection;
-use Drupal\Core\Database\Database;
-use Drupal\Core\Routing\MatcherDumper;
-use Drupal\Tests\Core\Routing\RoutingFixtures;
 
 /**
  * Confirm that the matcher dumper is functioning properly.
@@ -33,6 +34,11 @@ class MatcherDumperTest extends KernelTestBase {
    */
   protected $state;
 
+  /**
+   * The logger.
+   */
+  protected TestLogger $logger;
+
   /**
    * {@inheritdoc}
    */
@@ -41,6 +47,7 @@ protected function setUp(): void {
 
     $this->fixtures = new RoutingFixtures();
     $this->state = new State(new KeyValueMemoryFactory());
+    $this->logger = new TestLogger();
   }
 
   /**
@@ -48,7 +55,7 @@ protected function setUp(): void {
    */
   public function testCreate() {
     $connection = Database::getConnection();
-    $dumper = new MatcherDumper($connection, $this->state);
+    $dumper = new MatcherDumper($connection, $this->state, $this->logger);
 
     $class_name = 'Drupal\Core\Routing\MatcherDumper';
     $this->assertInstanceOf($class_name, $dumper);
@@ -59,7 +66,7 @@ public function testCreate() {
    */
   public function testAddRoutes() {
     $connection = Database::getConnection();
-    $dumper = new MatcherDumper($connection, $this->state);
+    $dumper = new MatcherDumper($connection, $this->state, $this->logger);
 
     $route = new Route('test');
     $collection = new RouteCollection();
@@ -80,7 +87,7 @@ public function testAddRoutes() {
    */
   public function testAddAdditionalRoutes() {
     $connection = Database::getConnection();
-    $dumper = new MatcherDumper($connection, $this->state);
+    $dumper = new MatcherDumper($connection, $this->state, $this->logger);
 
     $route = new Route('test');
     $collection = new RouteCollection();
@@ -108,7 +115,7 @@ public function testAddAdditionalRoutes() {
    */
   public function testDump() {
     $connection = Database::getConnection();
-    $dumper = new MatcherDumper($connection, $this->state, 'test_routes');
+    $dumper = new MatcherDumper($connection, $this->state, $this->logger, 'test_routes');
 
     $route = new Route('/test/{my}/path');
     $route->setOption('compiler_class', RouteCompiler::class);
@@ -143,7 +150,7 @@ public function testDump() {
    */
   public function testMenuMasksGeneration() {
     $connection = Database::getConnection();
-    $dumper = new MatcherDumper($connection, $this->state, 'test_routes');
+    $dumper = new MatcherDumper($connection, $this->state, $this->logger, 'test_routes');
 
     $collection = new RouteCollection();
     $collection->add('test_route_1', new Route('/test-length-3/{my}/path'));
diff --git a/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php b/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php
index dc3b90c07d62..cb30230de412 100644
--- a/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\KernelTests\Core\Routing;
 
+use ColinODell\PsrTestLogger\TestLogger;
 use Drupal\Core\Cache\MemoryBackend;
 use Drupal\Core\Database\Database;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
@@ -87,6 +88,13 @@ class RouteProviderTest extends KernelTestBase {
    */
   protected $cacheTagsInvalidator;
 
+  /**
+   * The test logger.
+   *
+   * @var \ColinODell\PsrTestLogger\TestLogger
+   */
+  protected TestLogger $logger;
+
   /**
    * {@inheritdoc}
    */
@@ -99,6 +107,8 @@ protected function setUp(): void {
     $this->pathProcessor = \Drupal::service('path_processor_manager');
     $this->cacheTagsInvalidator = \Drupal::service('cache_tags.invalidator');
     $this->installEntitySchema('path_alias');
+
+    $this->logger = new TestLogger();
   }
 
   /**
@@ -165,7 +175,7 @@ public function testExactPathMatch() {
 
     $this->fixtures->createTables($connection);
 
-    $dumper = new MatcherDumper($connection, $this->state, 'test_routes');
+    $dumper = new MatcherDumper($connection, $this->state, $this->logger, 'test_routes');
     $dumper->addRoutes($this->fixtures->sampleRouteCollection());
     $dumper->dump();
 
@@ -189,7 +199,7 @@ public function testOutlinePathMatch() {
 
     $this->fixtures->createTables($connection);
 
-    $dumper = new MatcherDumper($connection, $this->state, 'test_routes');
+    $dumper = new MatcherDumper($connection, $this->state, $this->logger, 'test_routes');
     $dumper->addRoutes($this->fixtures->complexRouteCollection());
     $dumper->dump();
 
@@ -242,7 +252,7 @@ public function testMixedCasePaths($path, $expected_route_name, $method = 'GET')
 
     $this->fixtures->createTables($connection);
 
-    $dumper = new MatcherDumper($connection, $this->state, 'test_routes');
+    $dumper = new MatcherDumper($connection, $this->state, $this->logger, 'test_routes');
     $dumper->addRoutes($this->fixtures->mixedCaseRouteCollection());
     $dumper->dump();
 
@@ -286,7 +296,7 @@ public function testDuplicateRoutePaths($path, $number, $expected_route_name = N
 
     $this->fixtures->createTables($connection);
 
-    $dumper = new MatcherDumper($connection, $this->state, 'test_routes');
+    $dumper = new MatcherDumper($connection, $this->state, $this->logger, 'test_routes');
     $dumper->addRoutes($this->fixtures->duplicatePathsRouteCollection());
     $dumper->dump();
 
@@ -308,7 +318,7 @@ public function testGetAllRoutes() {
 
     $this->fixtures->createTables($connection);
 
-    $dumper = new MatcherDumper($connection, $this->state, 'test_routes');
+    $dumper = new MatcherDumper($connection, $this->state, $this->logger, 'test_routes');
     $dumper->addRoutes($this->fixtures->SampleRouteCollection());
     $dumper->dump();
 
@@ -334,7 +344,7 @@ public function testOutlinePathMatchTrailingSlash() {
 
     $this->fixtures->createTables($connection);
 
-    $dumper = new MatcherDumper($connection, $this->state, 'test_routes');
+    $dumper = new MatcherDumper($connection, $this->state, $this->logger, 'test_routes');
     $dumper->addRoutes($this->fixtures->complexRouteCollection());
     $dumper->dump();
 
@@ -368,7 +378,7 @@ public function testOutlinePathMatchDefaults() {
       'value' => 'poink',
     ]));
 
-    $dumper = new MatcherDumper($connection, $this->state, 'test_routes');
+    $dumper = new MatcherDumper($connection, $this->state, $this->logger, 'test_routes');
     $dumper->addRoutes($collection);
     $dumper->dump();
 
@@ -407,7 +417,7 @@ public function testOutlinePathMatchDefaultsCollision() {
     ]));
     $collection->add('narf', new Route('/some/path/here'));
 
-    $dumper = new MatcherDumper($connection, $this->state, 'test_routes');
+    $dumper = new MatcherDumper($connection, $this->state, $this->logger, 'test_routes');
     $dumper->addRoutes($collection);
     $dumper->dump();
 
@@ -447,7 +457,7 @@ public function testOutlinePathMatchDefaultsCollision2() {
     $collection->add('narf', new Route('/some/path/here'));
     $collection->add('eep', new Route('/something/completely/different'));
 
-    $dumper = new MatcherDumper($connection, $this->state, 'test_routes');
+    $dumper = new MatcherDumper($connection, $this->state, $this->logger, 'test_routes');
     $dumper->addRoutes($collection);
     $dumper->dump();
 
@@ -486,7 +496,7 @@ public function testOutlinePathMatchDefaultsCollision3() {
     $collection->add('narf', new Route('/some/here/path'));
     $collection->add('eep', new Route('/something/completely/different'));
 
-    $dumper = new MatcherDumper($connection, $this->state, 'test_routes');
+    $dumper = new MatcherDumper($connection, $this->state, $this->logger, 'test_routes');
     $dumper->addRoutes($collection);
     $dumper->dump();
 
@@ -521,7 +531,7 @@ public function testOutlinePathMatchZero() {
     $collection = new RouteCollection();
     $collection->add('poink', new Route('/some/path/{value}'));
 
-    $dumper = new MatcherDumper($connection, $this->state, 'test_routes');
+    $dumper = new MatcherDumper($connection, $this->state, $this->logger, 'test_routes');
     $dumper->addRoutes($collection);
     $dumper->dump();
 
@@ -553,7 +563,7 @@ public function testOutlinePathNoMatch() {
 
     $this->fixtures->createTables($connection);
 
-    $dumper = new MatcherDumper($connection, $this->state, 'test_routes');
+    $dumper = new MatcherDumper($connection, $this->state, $this->logger, 'test_routes');
     $dumper->addRoutes($this->fixtures->complexRouteCollection());
     $dumper->dump();
 
@@ -578,7 +588,7 @@ public function testRouteCaching() {
 
     $this->fixtures->createTables($connection);
 
-    $dumper = new MatcherDumper($connection, $this->state, 'test_routes');
+    $dumper = new MatcherDumper($connection, $this->state, $this->logger, 'test_routes');
     $dumper->addRoutes($this->fixtures->sampleRouteCollection());
     $dumper->addRoutes($this->fixtures->complexRouteCollection());
     $dumper->dump();
@@ -653,7 +663,7 @@ public function testRouteByName() {
 
     $this->fixtures->createTables($connection);
 
-    $dumper = new MatcherDumper($connection, $this->state, 'test_routes');
+    $dumper = new MatcherDumper($connection, $this->state, $this->logger, 'test_routes');
     $dumper->addRoutes($this->fixtures->sampleRouteCollection());
     $dumper->dump();
 
@@ -702,7 +712,7 @@ public function testGetRoutesByPatternWithLongPatterns() {
     $this->assertCount(0, $candidates);
 
     // Add a matching route and dump it.
-    $dumper = new MatcherDumper($connection, $this->state, 'test_routes');
+    $dumper = new MatcherDumper($connection, $this->state, $this->logger, 'test_routes');
     $collection = new RouteCollection();
     $collection->add('long_pattern', new Route('/test/{v1}/test2/{v2}/test3/{v3}/{v4}/{v5}/{v6}/test4'));
     $dumper->addRoutes($collection);
diff --git a/core/tests/Drupal/Tests/Core/Cron/CronSuspendQueueDelayTest.php b/core/tests/Drupal/Tests/Core/Cron/CronSuspendQueueDelayTest.php
index f74ef7676eff..6ac8367a995d 100644
--- a/core/tests/Drupal/Tests/Core/Cron/CronSuspendQueueDelayTest.php
+++ b/core/tests/Drupal/Tests/Core/Cron/CronSuspendQueueDelayTest.php
@@ -70,7 +70,7 @@ protected function setUp(): void {
       'queue_config' => [],
     ];
 
-    // Capture logs to watchdog_exception().
+    // Capture error logs.
     $config = $this->createMock(ImmutableConfig::class);
     $config->expects($this->any())
       ->method('get')
-- 
GitLab