phpibm-cloudserverless-frameworksentryopenwhisk

PHP SDK not sending errors to Sentry when invoked from IBM Cloud Functions


I am using Serverless framework to deploy my PHP code as IBM Cloud Function.

Here is the code from the action PHP file:

function main($args): array {

    Sentry\init(['dsn' => 'SENTRY_DSN' ]);

    try {
        throw new \Exception('Some error')
    } catch (\Throwable $exception) {
        Sentry\captureException($exception);
    }
}

And this is the serverless.yml file:

service: cloudfunc

provider:
  name: openwhisk
  runtime: php

package:
  individually: true
  exclude:
    - "**"
  include:
    - "vendor/**"

functions:
    test-sentry:
    handler: actions/test-sentry.main
    annotations:
        raw-http: true
    events:
        - http:
            path: /test-sentry
            method: post
            resp: http
    package:
        include:
        - actions/test-sentry.php

plugins:
  - serverless-openwhisk

When I test the action handler from my local environment(NGINX/PHP Docker containers) the errors are being sent to Sentry.

But when I try to invoke the action from IBM Cloud nothing appears in the Sentry console.

Edit:

After some time trying to investigate the source of the problem I saw that its related with the async nature of sending the http request to Sentry(I have other libraries that make HTTP/TCP connections to Loggly, RabbitMQ, MySQL and they all work as expected):

vendor/sentry/sentry/src/Transport/HttpTransport.php

in the send method where the actual http request is being sent:

public function send(Event $event): ?string
    {
        $request = $this->requestFactory->createRequest(
            'POST',
            sprintf('/api/%d/store/', $this->config->getProjectId()),
            ['Content-Type' => 'application/json'],
            JSON::encode($event)
        );

        $promise = $this->httpClient->sendAsyncRequest($request);

        //The promise state here is "pending"
        //This line here is being logged in the stdout of the invoked action
        var_dump($promise->getState());

        // This function is defined in-line so it doesn't show up for type-hinting
        $cleanupPromiseCallback = function ($responseOrException) use ($promise) {

            //The promise state here is "fulfilled"
            //This line here is never logged in the stdout of the invoked action
            //Like the execution never happens here
            var_dump($promise->getState());

            $index = array_search($promise, $this->pendingRequests, true);

            if (false !== $index) {
                unset($this->pendingRequests[$index]);
            }

            return $responseOrException;
        };

        $promise->then($cleanupPromiseCallback, $cleanupPromiseCallback);

        $this->pendingRequests[] = $promise;

        return $event->getId();
    }

Solution

  • The requests that are registered asynchronously are sent in the destructor of the HttpTransport instance or when PHP shuts down as a shutdown function is registered. In OpenWhisk we never shut down as we run in a never-ending loop until the Docker container is killed.

    Update: You can now call $client-flush() and don't need to worry about reflection.

    main() now looks like this:

    function main($args): array {
    
        Sentry\init(['dsn' => 'SENTRY_DSN' ]);
    
        try {
            throw new \Exception('Some error')
        } catch (\Throwable $exception) {
            Sentry\captureException($exception);
        }
    
        $client = Sentry\State\Hub::getCurrent()->getClient();
        $client->flush();
    
        return [
            'body' => ['result' => 'ok']
        ];
    }
    

    Original explanation:

    As a result, to make this work, we need to call the destructor of the $transport property of the Hub's $client. Unfortunately, this private, so the easiest way to do this is to use reflection to make it visible and then call it:

    $client = Sentry\State\Hub::getCurrent()->getClient();
    $property = (new ReflectionObject($client))->getProperty('transport');
    $property->setAccessible(true);
    $transport = $property->getValue($client);
    $transport->__destruct();
    

    This will make the $transport property visible so that we can retrieve it and call its destructor which will in turn call cleanupPendingRequests() that will then send the requests to sentry.io.

    The main() therefore looks like this:

    function main($args): array {
    
        Sentry\init(['dsn' => 'SENTRY_DSN' ]);
    
        try {
            throw new \Exception('Some error')
        } catch (\Throwable $exception) {
            Sentry\captureException($exception);
        }
    
        $client = Sentry\State\Hub::getCurrent()->getClient();
        $property = (new ReflectionObject($client))->getProperty('transport');
        $property->setAccessible(true);
        $transport = $property->getValue($client);
        $transport->__destruct();
    
        return [
            'body' => ['result' => 'ok']
        ];
    } 
    

    Incidentally, I wonder if this Sentry SDK works with Swoole?