How to catch and log all exceptions in an Apigility ZF2 application?

2.9k Views Asked by At

I want to build an error handling & logging mecanism into an Apigility Zend Framework 2 aplication and catch & log all exceptions.

After some research I found a Stack Overflow answer with a solution, that seemed exactly to meet this requirements. Here is the code from the answer (with some minor naming and formatting modifications):

Module.php

...

use Zend\Mvc\ModuleRouteListener;
use Zend\Log\Logger;
use Zend\Log\Writer\Stream;

...

class Module implements ApigilityProviderInterface
{

    public function onBootstrap(MvcEvent $mvcEvent)
    {
        $eventManager = $mvcEvent->getApplication()->getEventManager();
        $moduleRouteListener = new ModuleRouteListener();
        $moduleRouteListener->attach($eventManager);
        /**
         * Log any Uncaught Exceptions, including all Exceptions in the stack
         */
        $sharedEventManager = $mvcEvent->getApplication()->getEventManager()->getSharedManager();
        $serviceManager = $mvcEvent->getApplication()->getServiceManager();
        $sharedEventManager->attach('Zend\Mvc\Application', MvcEvent::EVENT_DISPATCH_ERROR,
            function($mvcEvent) use ($serviceManager) {
                if ($mvcEvent->getParam('exception')){
                    $exception = $mvcEvent->getParam('exception');
                    do {
                        $serviceManager->get('Logger')->crit(
                            sprintf(
                               "%s:%d %s (%d) [%s]\n", 
                                $exception->getFile(), 
                                $exception->getLine(), 
                                $exception->getMessage(), 
                                $exception->getCode(), 
                                get_class($exception)
                            )
                        );
                    }
                    while($exception = $exception->getPrevious());
                }
            }
        );
    }

    ...

    public function getServiceConfig() {
        return array(
            'factories' => array(
                // V1
                ...
                'Logger' => function($sm){
                    $logger = new Logger;
                    $writer = new Stream('/var/log/httpd/sandbox-log');
                    $logger->addWriter($writer);
                    return $logger;
                },
            ),
            ...
        );
    }

}

So now I've tried this out (with a simple throw new \Exception('foo')) at several places in the code (in a Resource, in a Service, and in a Mapper class) and expected to get the exceptions cached and logged into the file I defiden for. But it isn't working.

Am I doing something wrong? What? How to get it working? How to catch and log all exceptions in an Apigility driven Zend Framework 2 application?


Additional info: An example of a place in the code, where an exception gets thrown:

class AddressResource extends AbstractResourceListener ...
{
    public function fetch($id) {
        throw new \Exception('fetch_EXCEPTION');
        $service = $this->getAddressService();
        $entity = $service->getAddress($id);
        return $entity;
    }
}

Additional info: The trace in the respose (when if set throw new \Exception('fetch_EXCEPTION'); in the BarResource#fetch(...)):

{
    "trace": [
        {
            "file": "/var/www/my-project/vendor/zfcampus/zf-rest/src/AbstractResourceListener.php",
            "line": 166,
            "function": "fetch",
            "class": "FooAPI\\V1\\Rest\\Bar\\BarResource",
            "type": "->",
            "args": [
                "1"
            ]
        },
        {
            "function": "dispatch",
            "class": "ZF\\Rest\\AbstractResourceListener",
            "type": "->",
            "args": [
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php",
            "line": 444,
            "function": "call_user_func",
            "args": [
                [
                    {},
                    "dispatch"
                ],
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php",
            "line": 205,
            "function": "triggerListeners",
            "class": "Zend\\EventManager\\EventManager",
            "type": "->",
            "args": [
                "fetch",
                {},
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zfcampus/zf-rest/src/Resource.php",
            "line": 541,
            "function": "trigger",
            "class": "Zend\\EventManager\\EventManager",
            "type": "->",
            "args": [
                {},
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zfcampus/zf-rest/src/RestController.php",
            "line": 483,
            "function": "fetch",
            "class": "ZF\\Rest\\Resource",
            "type": "->",
            "args": [
                "1"
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/AbstractRestfulController.php",
            "line": 366,
            "function": "get",
            "class": "ZF\\Rest\\RestController",
            "type": "->",
            "args": [
                "1"
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zfcampus/zf-rest/src/RestController.php",
            "line": 332,
            "function": "onDispatch",
            "class": "Zend\\Mvc\\Controller\\AbstractRestfulController",
            "type": "->",
            "args": [
                {}
            ]
        },
        {
            "function": "onDispatch",
            "class": "ZF\\Rest\\RestController",
            "type": "->",
            "args": [
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php",
            "line": 444,
            "function": "call_user_func",
            "args": [
                [
                    {},
                    "onDispatch"
                ],
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php",
            "line": 205,
            "function": "triggerListeners",
            "class": "Zend\\EventManager\\EventManager",
            "type": "->",
            "args": [
                "dispatch",
                {},
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/AbstractController.php",
            "line": 118,
            "function": "trigger",
            "class": "Zend\\EventManager\\EventManager",
            "type": "->",
            "args": [
                "dispatch",
                {},
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/AbstractRestfulController.php",
            "line": 300,
            "function": "dispatch",
            "class": "Zend\\Mvc\\Controller\\AbstractController",
            "type": "->",
            "args": [
                {},
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zendframework/zendframework/library/Zend/Mvc/DispatchListener.php",
            "line": 93,
            "function": "dispatch",
            "class": "Zend\\Mvc\\Controller\\AbstractRestfulController",
            "type": "->",
            "args": [
                {},
                {}
            ]
        },
        {
            "function": "onDispatch",
            "class": "Zend\\Mvc\\DispatchListener",
            "type": "->",
            "args": [
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php",
            "line": 444,
            "function": "call_user_func",
            "args": [
                [
                    {},
                    "onDispatch"
                ],
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php",
            "line": 205,
            "function": "triggerListeners",
            "class": "Zend\\EventManager\\EventManager",
            "type": "->",
            "args": [
                "dispatch",
                {},
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zendframework/zendframework/library/Zend/Mvc/Application.php",
            "line": 314,
            "function": "trigger",
            "class": "Zend\\EventManager\\EventManager",
            "type": "->",
            "args": [
                "dispatch",
                {},
                {}
            ]
        },
        {
            "file": "/var/www/my-project/public/index.php",
            "line": 56,
            "function": "run",
            "class": "Zend\\Mvc\\Application",
            "type": "->",
            "args": []
        }
    ],
    "type": "http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html",
    "title": "Internal Server Error",
    "status": 500,
    "detail": "fetch_EXCEPTION"
}
3

There are 3 best solutions below

5
On

You connected your listener to the MvcEvent::EVENT_DISPATCH_ERROR event. Are you sure that you throw your new \Exception('foo') during dispatch (i.e. in a controller). Also in the answer you linked to they mention that this solution is for catching errors/exceptions thrown in a controller.

If you for example throw an exception while rendering your listener will never be triggered. In those cases you would need to listen to MvcEvent::EVENT_RENDER_ERROR.

I wonder if this setup is the best way to do it. Maybe you should search for other examples instead of simply following/copying an answer from StackOverflow.

EDIT:

If you are also using the ApiProblem module for Apigility then it could be that the ApiProblemListener is triggered on a MvcEvent::EVENT_DISPATCH_ERROR event before your own listener.

In the onDispatchError method the ApiProblemListener returns a response object and that might be the reason that other events (with lower priority) are not triggered at all after this.

The ApiProblemListener is attached like this:

$this->listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, array($this, 'onDispatchError'), 100);

Try to raise the priority for your listener to a value above the 100 from the ApiProblemListener. Then you will probably have success.

2
On

We are currently using the following code successfully to capture all error responses from Apigility:

$app = $event->getTarget();
$em = $app->getEventManager();

$sendResponseListener = $app->getServiceManager()->get('SendResponseListener');
$sendResponseListener->getEventManager()->attach(SendResponseEvent::EVENT_SEND_RESPONSE,  function(SendResponseEvent $event) {
    $response = $event->getResponse();
    if ($response instanceof ApiProblemResponse) {
          $error = $response->getApiProblem()->toArray();
          // inspect $error array and log the information you want
    }
});
0
On

I face the same problem to logging any exception happened,

I try to attach my own listener to MvcEvent::EVENT_DISPATCH_ERROR even with high priority[3000] not working, after some research and read Apigility code I discover that any thrown exception will be catch by ApiProblemListener and create new ApiProblem from this exception information into onRender method

The workaround to solve problem and log any exception is override the AbstractResourceListener and create your own ResourceListener and force any Resource class extend it,

your own ResourceListener must override dispatch method and call parent method to catch the thrown exception and log it then return new Response

Example:

<?php

namespace API\V1\Listener;

class MyOwnListener  extends AbstractResourceListener
{

    /**
     * {@inheritdoc}
     */
    public function dispatch(ResourceEvent $event)
    {
        try {
            $response = parent::dispatch($event);
        } catch (\Throwable $exception) {
            // catch thrown exception
            // then return new APIProblem with message you want 
            $response = new ApiProblem(500, 'error message');
        }
        return $response;
    }
}