Simon Fredsted
Simon Fredsted

Reputation: 976

Make adding custom instrumentation in Sentry less verbose

I'm playing around with instrumentation using Sentry and I'm so discouraged by how many lines of code I have to add everywhere.

According to the Sentry docs, I have to add all these lines every time I want to measure something:

        $sentryTransactionContext = (new TransactionContext('Something that needs measuring'));
        $sentryTransactionContext->setOp('http.server');
        $sentryTransaction = startTransaction($sentryTransactionContext);
        SentrySdk::getCurrentHub()->setSpan($sentryTransaction);
        $spanContext = (new SpanContext());
        $spanContext->setOp('something.that.needs.measuring');
        $span1 = $sentryTransaction->startChild($spanContext);
        \Sentry\SentrySdk::getCurrentHub()->setSpan($span1);

        // Do something that needs to be measured...

        $span1->finish();
        SentrySdk::getCurrentHub()->setSpan($sentryTransaction);
        $sentryTransaction->finish();

Is all that stuff really supposed to go in all my different Controller methods, or places where I need to measure how long a piece of code takes? It would be so much duplicate code.

Ideally, I would like to just do this:

public function create(HttpRequest $request)
{
        sentry_measure_start('slow.task');

        // Something slow that needs to be measured

        sentry_measure_stop('slow.task');
}

Is that possible?

Upvotes: 1

Views: 119

Answers (2)

noah1400
noah1400

Reputation: 1509

You could write a Service class that handles and simplifies the syntax for starting and stopping Sentry transactions and spans.

  1. Create a app/Services/SentryMeasureService.php

    namespace App\Services;
    
    use Sentry\Tracing\TransactionContext;
    use Sentry\Tracing\SpanContext;
    use Sentry\SentrySdk;
    
    class SentryMeasure {
        private static $transactions = [];
        private static $spans = [];
    
        /**
         * Start measuring a transaction or span
         * 
         * @param string $name Unique identifier for the transaction or span
         * @param string $type Type of measurement (transaction or span)
         * @param string|null $parentName Parent transaction/span name (optional)
         */
        public static function start(string $name, string $type = 'transaction', ?string $parentName = null)
        {
            try {
                if ($type === 'transaction') {
                    // Create and start a new transaction
                    $transactionContext = new \Sentry\Tracing\TransactionContext($name);
                    $transactionContext->setOp('http.server');
                    $transaction = \Sentry\startTransaction($transactionContext);
    
                    \Sentry\SentrySdk::getCurrentHub()->setSpan($transaction);
    
                    self::$transactions[$name] = $transaction;
                } elseif ($type === 'span') {
                    if (!isset(self::$transactions[$parentName])) {
                        throw new \Exception("Parent transaction '{$parentName}' not found");
                    }
    
                    $parentTransaction = self::$transactions[$parentName];
    
                    $spanContext = new \Sentry\Tracing\SpanContext();
                    $spanContext->setOp($name);
                    $span = $parentTransaction->startChild($spanContext);
    
                    \Sentry\SentrySdk::getCurrentHub()->setSpan($span);
    
                    self::$spans[$name] = $span;
                } else {
                    throw new \InvalidArgumentException("Invalid measurement type. Use 'transaction' or 'span'.");
                }
            } catch (\Exception $e) {
                error_log("Sentry measurement start error: " . $e->getMessage());
            }
        }
    
        /**
         * Stop measuring a transaction or span
         * 
         * @param string $name Unique identifier for the transaction or span to stop
         * @param string $type Type of measurement (transaction or span)
         */
        public static function stop(string $name, string $type = 'transaction')
        {
            try {
                if ($type === 'transaction') {
                    if (isset(self::$transactions[$name])) {
                        $transaction = self::$transactions[$name];
                        $transaction->finish();
    
                        unset(self::$transactions[$name]);
                    }
                } elseif ($type === 'span') {
                    if (isset(self::$spans[$name])) {
                        $span = self::$spans[$name];
                        $span->finish();
    
                        unset(self::$spans[$name]);
    
                        if (!empty(self::$transactions)) {
                            $lastTransactionName = array_key_last(self::$transactions);
                            $lastTransaction = self::$transactions[$lastTransactionName];
                            \Sentry\SentrySdk::getCurrentHub()->setSpan($lastTransaction);
                        }
                    }
                } else {
                    throw new \InvalidArgumentException("Invalid measurement type. Use 'transaction' or 'span'.");
                }
            } catch (\Exception $e) {
                error_log("Sentry measurement stop error: " . $e->getMessage());
            }
        }
    }
    
  2. Create a app/helpers.php file to implement your helper functions

    use App\Services\SentryMeasureService;
    
    if (!function_exists('sentry_measure_start')) {
        function sentry_measure_start(string $name, ?string $parentName = null)
        {
            SentryMeasureService::start($name, $parentName === null ? 'transaction' : 'span', $parentName);
        }
    }
    
    if (!function_exists('sentry_measure_stop')) {
        function sentry_measure_stop(string $name)
        {
            SentryMeasureService::stop($name, 'span');
            SentryMeasureService::stop($name, 'transaction');
        }
    }
    
  3. To make the helper functions visible you need to modify your composer.json more specifically the autoload key. Add a files array

    inside autoload.

    "autoload": {
        "files": [
            "app/helpers.php"
        ],
        "classmap": [
            "database/seeds",
            "database/factories"
        ],
        "psr-4": {
            "App\\": "app/"
        }
    },
    
  4. Once you added the file you need to dump the autloader

    composer dump-autoload
    

Now you should be able to use the functions the way you suggested:

public function create(Request $request)
{
    sentry_measure_start('slow.task');

    sleep(2); // example of a slow operation

    sentry_measure_stop('slow.task');
}

Warning

If you are using this solution in combination with laravel octane it is important that there is a sentry_measure_stop('slow.task'); for every sentry_measure_start('slow.task');. Octane keeps your application in memory between requests therefore, adding and not removing data to a statically maintained array will result in a memory leak!

Laravel Octane Docs

Upvotes: 2

Simon Fredsted
Simon Fredsted

Reputation: 976

I found that Sentry's SDK has a less verbose static method that apparently isn't mentioned in the documentation. Here's how I use it. It's still a little cumbersome, but tolerable.

        $transaction = SentrySdk::getCurrentHub()->getTransaction()
            ?: startTransaction(TransactionContext::make()->setName('slow.task')->setOp('slow.task1'));
        $child = $transaction->startChild(SpanContext::make()->setOp('slow.task1.subtask1'));

        // measure something

        $child->finish();
        $transaction->finish();

Upvotes: 0

Related Questions