community/feature-request#12 - Allow named logging channels
authorTim Otten <totten@civicrm.org>
Fri, 16 Apr 2021 01:49:27 +0000 (18:49 -0700)
committerTim Otten <totten@civicrm.org>
Fri, 16 Apr 2021 02:00:10 +0000 (19:00 -0700)
Overview
----------------------------------------

Make it easier to route log messages based on their topic (e.g. CiviContribute-related logs vs CiviMail-related logs).

Before
------

`Civi::log()` always returns the same instance of `LoggerInterface`, with no
clear way to differentiate logs of different business subsystems.

After
-----

`Civi::log(...)` allows you to optionally request a `LoggerInterface` for a specific theme, e.g.

```php
Civi::log('mail')->error('Failed to connect to SMTP server');
Civi::log('ipn')->warning('Transaction rejected by payment processor');
```

Technical Details
-----------------

A few things going on here:

* Extensions may start using their own logs (`Civi::log('myext')`) without any special effort.
* It is possible to replace or customize specific logs by defining a service `log.CHANNEL_NAME`.
* The `psr_log_manager` is a service. An extension like https://lab.civicrm.org/extensions/monolog/
  can replace the `psr_log_manager` and use the channel-name in its own way.

There is a limitation here in that the list of channels is open-ended.  It
will be impossible to (eg) detect that a log-user has made a typo in the
channel-name.  However, this seems like the better trade-off if the
alternative is that extensions face races during
installation/uninstallation.

Civi.php
Civi/Core/Container.php
Civi/Core/LogManager.php [new file with mode: 0644]

index f916756883ed7ae8c40756847a70a471576490df..a018bb54d9d2ef2ff07fd2f682688783a3b13de7 100644 (file)
--- a/Civi.php
+++ b/Civi.php
@@ -82,10 +82,16 @@ class Civi {
   }
 
   /**
-   * @return \CRM_Core_Error_Log
+   * Find or create a logger.
+   *
+   * @param string $channel
+   *   Symbolic name (or channel) of the intended log.
+   *   This should correlate to a service "log.{NAME}".
+   *
+   * @return \Psr\Log\LoggerInterface
    */
-  public static function log() {
-    return Civi\Core\Container::singleton()->get('psr_log');
+  public static function log($channel = 'default') {
+    return \Civi\Core\Container::singleton()->get('psr_log_manager')->getLog($channel);
   }
 
   /**
index 39749a81bbda03b17d9ef0b69cf97b1c39261513..c4f8ca72ab9ac8ad1535b28cc8b013a87fa3b83e 100644 (file)
@@ -150,6 +150,9 @@ class Container {
       ->setFactory('CRM_Cxn_BAO_Cxn::createRegistrationClient')->setPublic(TRUE);
 
     $container->setDefinition('psr_log', new Definition('CRM_Core_Error_Log', []))->setPublic(TRUE);
+    $container->setDefinition('psr_log_manager', new Definition('Civi\Core\LogManager', []))->setPublic(TRUE);
+    // With the default log-manager, you may overload a channel by defining a service, e.g.
+    // $container->setDefinition('log.ipn', new Definition('CRM_Core_Error_Log', []))->setPublic(TRUE);
 
     $basicCaches = [
       'js_strings' => 'js_strings',
diff --git a/Civi/Core/LogManager.php b/Civi/Core/LogManager.php
new file mode 100644 (file)
index 0000000..2c181a0
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Core;
+
+/**
+ * The LogManager will provide instances of "LoggerInterface".
+ *
+ * @package Civi\Core
+ */
+class LogManager {
+
+  const DEFAULT_LOGGER = 'psr_log';
+
+  private $channels = [];
+
+  /**
+   * Find or create a logger.
+   *
+   * This implementation will look for a service "log.{NAME}". If none is defined,
+   * then it will fallback to the "psr_log" service.
+   *
+   * @param string $channel
+   *   Symbolic name of the intended log.
+   *   This should correlate to a service "log.{NAME}".
+   *
+   * @return \Psr\Log\LoggerInterface
+   */
+  public function getLog($channel = 'default') {
+    if (!isset($this->channels[$channel])) {
+      $c = \Civi::container();
+      $svc = "log." . $channel;
+      $this->channels[$channel] = $c->has($svc) ? $c->get($svc) : $c->get(self::DEFAULT_LOGGER);
+    }
+    return $this->channels[$channel];
+  }
+
+}