API - Display all logged warnings, errors, etc. in debug output and APIv4 explorer.
authorColeman Watts <coleman@civicrm.org>
Tue, 5 May 2020 19:19:08 +0000 (15:19 -0400)
committerColeman Watts <coleman@civicrm.org>
Tue, 5 May 2020 19:19:08 +0000 (15:19 -0400)
CRM/Core/Error.php
CRM/Core/Error/Log.php
Civi.php
Civi/API/LogObserver.php [new file with mode: 0644]
Civi/API/Subscriber/DebugSubscriber.php [moved from Civi/API/Subscriber/XDebugSubscriber.php with 61% similarity]
Civi/Core/Container.php
ang/api4Explorer/Explorer.html
ang/api4Explorer/Explorer.js

index a36bb5ce7dad41a6e8139c2144d19df55ece5b4c..206570e51b93bce6f8201de15dee7fef28432a32 100644 (file)
@@ -608,7 +608,7 @@ class CRM_Core_Error extends PEAR_ErrorStack {
    *
    * @param string $prefix
    *
-   * @return Log
+   * @return Log_file
    */
   public static function createDebugLogger($prefix = '') {
     self::generateLogFileName($prefix);
index 61458ba900725c44519d9306da5cc51e822235d8..6b7fd20c8e759bc89247f2900618efcfd5581752 100644 (file)
  */
 class CRM_Core_Error_Log extends \Psr\Log\AbstractLogger {
 
+  /**
+   * @var array
+   */
+  public $map;
+
   /**
    * CRM_Core_Error_Log constructor.
    */
index 12b2119b45bc846ca7472b3ac6a518c5140532fb..ebd09e8afec737e387d067a398a7a6c79e51f439 100644 (file)
--- a/Civi.php
+++ b/Civi.php
@@ -75,7 +75,7 @@ class Civi {
   }
 
   /**
-   * @return \Psr\Log\LoggerInterface
+   * @return \CRM_Core_Error_Log
    */
   public static function log() {
     return Civi\Core\Container::singleton()->get('psr_log');
diff --git a/Civi/API/LogObserver.php b/Civi/API/LogObserver.php
new file mode 100644 (file)
index 0000000..b92eaa6
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+
+
+namespace Civi\API;
+
+/**
+ * API Error Log Observer
+ *
+ * @see \CRM_Core_Error_Log
+ * @see \Civi\API\Subscriber\DebugSubscriber
+ *
+ * @package Civi\API
+ */
+class LogObserver extends \Log_observer {
+
+  /**
+   * @var array
+   */
+  private static $messages = [];
+
+  /**
+   * @see \Log::_announce
+   * @param array $event
+   */
+  public function notify($event) {
+    $levels = \Civi::log()->map;
+    $event['level'] = array_search($event['priority'], $levels);
+    // Extract [civi.tag] from message string
+    // As noted in \CRM_Core_Error_Log::log() the $context array gets prematurely converted to string with print_r() so we have to un-flatten it here
+    if (preg_match('/^(.*)\s*Array\s*\(\s*\[civi\.(\w+)] => (\w+)\s*\)/', $event['message'], $message)) {
+      $event['message'] = $message[1];
+      $event[$message[2]] = $message[3];
+    }
+    self::$messages[] = $event;
+  }
+
+  /**
+   * @return array
+   */
+  public function getMessages() {
+    return self::$messages;
+  }
+
+}
similarity index 61%
rename from Civi/API/Subscriber/XDebugSubscriber.php
rename to Civi/API/Subscriber/DebugSubscriber.php
index aec0c1259e935c838fe4b51ee7e651f1503daae0..88f529f972e916ee85726dbc48100473f9bb351f 100644 (file)
@@ -18,17 +18,34 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  * Class XDebugSubscriber
  * @package Civi\API\Subscriber
  */
-class XDebugSubscriber implements EventSubscriberInterface {
+class DebugSubscriber implements EventSubscriberInterface {
+
+  /**
+   * @var \Civi\API\LogObserver
+   */
+  private $debugLog;
 
   /**
    * @return array
    */
   public static function getSubscribedEvents() {
     return [
-      Events::RESPOND => ['onApiRespond', Events::W_LATE],
+      Events::PREPARE => ['onApiPrepare', 999],
+      Events::RESPOND => ['onApiRespond', -999],
     ];
   }
 
+  public function onApiPrepare(\Civi\API\Event\PrepareEvent $event) {
+    $apiRequest = $event->getApiRequest();
+    if (!isset($this->debugLog)
+      && !empty($apiRequest['params']['debug'])
+      && (empty($apiRequest['params']['check_permissions']) || \CRM_Core_Permission::check('view debug output'))
+    ) {
+      $this->debugLog = new \Civi\API\LogObserver();
+      \CRM_Core_Error::createDebugLogger()->attach($this->debugLog);
+    }
+  }
+
   /**
    * @param \Civi\API\Event\RespondEvent $event
    *   API response event.
@@ -36,9 +53,7 @@ class XDebugSubscriber implements EventSubscriberInterface {
   public function onApiRespond(\Civi\API\Event\RespondEvent $event) {
     $apiRequest = $event->getApiRequest();
     $result = $event->getResponse();
-    if (
-      function_exists('xdebug_time_index')
-      && !empty($apiRequest['params']['debug'])
+    if (!empty($apiRequest['params']['debug'])
       && (empty($apiRequest['params']['check_permissions']) || \CRM_Core_Permission::check('view debug output'))
     ) {
       if (is_a($result, '\Civi\Api4\Generic\Result')) {
@@ -53,9 +68,14 @@ class XDebugSubscriber implements EventSubscriberInterface {
       else {
         return;
       }
-      $debug['peakMemory'] = xdebug_peak_memory_usage();
-      $debug['memory'] = xdebug_memory_usage();
-      $debug['timeIndex'] = xdebug_time_index();
+      if (isset($this->debugLog) && $this->debugLog->getMessages()) {
+        $debug['log'] = $this->debugLog->getMessages();
+      }
+      if (function_exists('xdebug_time_index')) {
+        $debug['peakMemory'] = xdebug_peak_memory_usage();
+        $debug['memory'] = xdebug_memory_usage();
+        $debug['timeIndex'] = xdebug_time_index();
+      }
       $event->setResponse($result);
     }
   }
index f0bc4a2bf0037cb99e9abab4680279028ee52cf3..7493eff1785db026b59b6f4982f320bdf85df49f 100644 (file)
@@ -410,7 +410,7 @@ class Container {
       \CRM_Utils_API_ReloadOption::singleton(),
       \CRM_Utils_API_MatchOption::singleton(),
     ]));
-    $dispatcher->addSubscriber(new \Civi\API\Subscriber\XDebugSubscriber());
+    $dispatcher->addSubscriber(new \Civi\API\Subscriber\DebugSubscriber());
     $kernel = new \Civi\API\Kernel($dispatcher);
 
     $reflectionProvider = new \Civi\API\Provider\ReflectionProvider($kernel);
index 56d6e727963ded5a017ae445f19958d3076ebe98..abff9b0162f619fd98f292b6de49d6b85da113ed 100644 (file)
       </div>
   </div>
   <div class="api4-explorer-row">
-      <div class="panel panel-warning explorer-code-panel">
+      <div class="panel panel-info explorer-code-panel">
         <ul class="panel-heading nav nav-tabs">
           <li role="presentation" ng-repeat="lang in ::langs" ng-class="{active: selectedTab.code === lang}">
             <a href ng-click="selectLang(lang)">
               <span ng-switch="status">
                 <i class="fa fa-fw fa-circle-o" ng-switch-when="default"></i>
                 <i class="fa fa-fw fa-check-circle" ng-switch-when="success"></i>
+                <i class="fa fa-fw fa-check-circle" ng-switch-when="warning"></i>
                 <i class="fa fa-fw fa-minus-circle" ng-switch-when="danger"></i>
-                <i class="fa fa-fw fa-spinner fa-pulse" ng-switch-when="warning"></i>
+                <i class="fa fa-fw fa-spinner fa-pulse" ng-switch-when="info"></i>
               </span>
               <span>{{:: ts('Result') }}</span>
             </a>
           </li>
           <li role="presentation" ng-if="::perm.accessDebugOutput" ng-class="{active: selectedTab.result === 'debug'}">
             <a href ng-click="selectedTab.result = 'debug'">
-              <i class="fa fa-fw fa-{{ debug ? 'bug' : 'circle-o' }}"></i>
+              <i class="fa fa-fw fa-{{ debug ? (status === 'warning' || status === 'danger' ? 'warning' : 'bug') : 'circle-o' }}"></i>
               <span>{{:: ts('Debug') }}</span>
             </a>
           </li>
index 40471bf9d853fa5b9b3c33d12f8371b3d853ecd2..97c6152e2355e3b0e5035d8e7fbe677853b35966 100644 (file)
     }
 
     $scope.execute = function() {
-      $scope.status = 'warning';
+      $scope.status = 'info';
       $scope.loading = true;
       $http.post(CRM.url('civicrm/ajax/api4/' + $scope.entity + '/' + $scope.action, {
         params: angular.toJson(getParams()),
         }
       }).then(function(resp) {
           $scope.loading = false;
-          $scope.status = 'success';
+          $scope.status = resp.data && resp.data.debug && resp.data.debug.log ? 'warning' : 'success';
           $scope.debug = debugFormat(resp.data);
           $scope.result = [formatMeta(resp.data), prettyPrintOne(_.escape(JSON.stringify(resp.data.values, null, 2)), 'js', 1)];
         }, function(resp) {