├── .idea └── laravel-new-relic.iml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── new-relic.php ├── resources └── views │ └── .gitkeep └── src ├── Commands └── NewRelicDeployCommand.php ├── Facades └── NewRelicTransaction.php ├── Helpers └── LoggableNewRelicFunctions.php ├── LaravelNewRelicServiceProvider.php ├── Middleware └── NewRelicMiddleware.php ├── NewRelicTransaction.php └── NewRelicTransactionHandler.php /.idea/laravel-new-relic.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.2.0 — Laravel 12 support - 2025-04-19 4 | 5 | ### What's Changed 6 | 7 | * Bump aglipanci/laravel-pint-action from 2.0.0 to 2.3.1 by @dependabot in https://github.com/jackwh/laravel-new-relic/pull/20 8 | * Bump aglipanci/laravel-pint-action from 2.3.1 to 2.4 by @dependabot in https://github.com/jackwh/laravel-new-relic/pull/22 9 | * Laravel 12.x Compatibility by @laravel-shift in https://github.com/jackwh/laravel-new-relic/pull/25 10 | * Fix #15 missing fallback for the empty event name. by @arku31 in https://github.com/jackwh/laravel-new-relic/pull/24 11 | * Bump dependabot/fetch-metadata from 1.6.0 to 2.2.0 by @dependabot in https://github.com/jackwh/laravel-new-relic/pull/23 12 | 13 | ### New Contributors 14 | 15 | * @laravel-shift made their first contribution in https://github.com/jackwh/laravel-new-relic/pull/25 16 | * @arku31 made their first contribution in https://github.com/jackwh/laravel-new-relic/pull/24 17 | 18 | **Full Changelog**: https://github.com/jackwh/laravel-new-relic/compare/v1.1.0...v1.2.0 19 | 20 | ## v1.1.0 — Laravel 11 support, improved standards - 2024-05-10 21 | 22 | ### What's Changed 23 | 24 | * Bump dependabot/fetch-metadata from 1.3.6 to 1.4.0 by @dependabot in https://github.com/JackWH/laravel-new-relic/pull/12 25 | * Bump dependabot/fetch-metadata from 1.4.0 to 1.5.1 by @dependabot in https://github.com/JackWH/laravel-new-relic/pull/13 26 | * Bump dependabot/fetch-metadata from 1.5.1 to 1.6.0 by @dependabot in https://github.com/JackWH/laravel-new-relic/pull/14 27 | * Feature/l11 strict standards by @fruitl00p in https://github.com/JackWH/laravel-new-relic/pull/19 28 | 29 | ### New Contributors 30 | 31 | * @fruitl00p made their first contribution in https://github.com/JackWH/laravel-new-relic/pull/19 32 | 33 | **Full Changelog**: https://github.com/JackWH/laravel-new-relic/compare/v1.0.3...v1.1.0 34 | 35 | ## v1.0.3 — Laravel 10 support, queue prefix fix - 2023-04-03 36 | 37 | ### What's Changed 38 | 39 | - Allow Laravel 10 by @crishoj in https://github.com/JackWH/laravel-new-relic/pull/10 40 | - Fix prefix does not work on handling queue transaction by @rbiya in https://github.com/JackWH/laravel-new-relic/pull/11 41 | 42 | ### New Contributors 43 | 44 | - @crishoj made their first contribution in https://github.com/JackWH/laravel-new-relic/pull/10 45 | - @rbiya made their first contribution in https://github.com/JackWH/laravel-new-relic/pull/11 46 | 47 | **Full Changelog**: https://github.com/JackWH/laravel-new-relic/compare/v1.0.2...v1.0.3 48 | 49 | ## v1.0.2 — Fix deployment command - 2022-06-03 50 | 51 | ### What's Changed 52 | 53 | - Fix deploy command by @ksimenic in https://github.com/JackWH/laravel-new-relic/pull/1 54 | 55 | ### New Contributors 56 | 57 | - @ksimenic made their first contribution in https://github.com/JackWH/laravel-new-relic/pull/1 58 | 59 | ## v1.0.1 — Updated README - 2022-05-21 60 | 61 | Added an extra comment to note about turning off `local` as a loggable environment. 62 | 63 | ## v1.0.0 — Initial Release - 2022-05-21 64 | 65 | This is the first release of the `jackwh/laravel-new-relic` package. 66 | 67 | Full details are available in [the README](README.md file). 68 | 69 | ## v1.0 70 | 71 | - Initial release. Enjoy! 72 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Jack Webb-Heller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel New Relic 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/jackwh/laravel-new-relic.svg?style=flat-square)](https://packagist.org/packages/jackwh/laravel-new-relic) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/workflow/status/jackwh/laravel-new-relic/run-tests?label=tests)](https://github.com/jackwh/laravel-new-relic/actions?query=workflow%3Arun-tests+branch%3Amain) 5 | [![GitHub Code Style Action Status](https://img.shields.io/github/workflow/status/jackwh/laravel-new-relic/Check%20&%20fix%20styling?label=code%20style)](https://github.com/jackwh/laravel-new-relic/actions?query=workflow%3A"Check+%26+fix+styling"+branch%3Amain) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/jackwh/laravel-new-relic.svg?style=flat-square)](https://packagist.org/packages/jackwh/laravel-new-relic) 7 | 8 | 9 | 10 | This package makes it simple to set up and monitor your [Laravel](https://laravel.com) application with [New Relic APM](https://newrelic.com/products/application-monitoring). 11 | 12 | New Relic provides some excellent low-level insights into your application. The [New Relic PHP agent](https://docs.newrelic.com/docs/apm/agents/php-agent/getting-started/introduction-new-relic-php/) is particularly useful in production environments, as it hooks in at a lower level than other monitoring services, and with little to no impact on performance. 13 | 14 | > **New Relic has a fully-featured [free plan](https://newrelic.com/pricing)** which is ideal for growing Laravel applications. This package isn't affiliated with them — I just built it because I've found the service very helpful whilst scaling my app, and wanted a more tailored solution for Laravel. 15 | > 16 | > Whilst New Relic can monitor a Laravel application out-of-the-box, this package reports transactions that are optimised for Laravel, and reported in a more consisted way. 17 | 18 | ## Installation 19 | 20 | To monitor your application in production you'll need a [New Relic](https://newrelic.com) API account, and you should [install the PHP monitoring agent](https://docs.newrelic.com/docs/apm/agents/php-agent/installation/php-agent-installation-overview/). You don't need to install New Relic in your development environment (unless you really want to). If the extension isn't detected the package will simulate calls to the New Relic PHP agent, and log each one so you can test before deploying. 21 | 22 | > If you're installing this on a server which is *already* being monitored by New Relic, **be aware this package reports transactions with different naming conventions than New Relic normally auto-detects**. If your existing New Relic data is very important to you, don't install this. 23 | 24 | To install the package, add it to your Laravel project with Composer: 25 | 26 | ```bash 27 | composer require jackwh/laravel-new-relic 28 | ``` 29 | 30 | Then publish the config file: 31 | 32 | ```bash 33 | php artisan vendor:publish --provider="JackWH\LaravelNewRelic\LaravelNewRelicServiceProvider" 34 | ``` 35 | 36 | > **That's it, you're done! The package is ready to go, and configured out-of-the-box.** 37 | 38 | ## How It Works 39 | 40 | #### The Service Provider 41 | 42 | Laravel will auto-discover the `LaravelNewRelicServiceProvider` class, which binds `NewRelicTransactionHandler` and `NewRelicTransaction` classes as [scoped singletons](https://laravel.com/docs/9.x/container#binding-scoped) to the service container. 43 | 44 | New Relic's transaction API only allows a single transaction to be active at a time. That's why the classes are loaded as singletons. Generally speaking, don't try to start a new transaction mid-way through the request lifecycle. 45 | 46 | #### Loggable Environments 47 | The package checks if New Relic is installed. If it's not found, you can log simulated transactions. 48 | 49 | In a loggable environment, the package will simulate calls it would normally make to New Relic's methods (e.g. `newrelic_start_transaction()`). These are loaded from the `LoggableNewRelicFunctions.php` helper file. You can check your logs to see what's happening under the hood. 50 | 51 | Don't worry if your logs don't show a "transaction ended" item, as New Relic automatically finishes them at the end of a request. This is only really important for long-running processes, like the queue handler. 52 | 53 | > Once you're happy logging is working as expected, you can comment out `local` in the `config/new-relic.php` file. 54 | > This is just intended to help you check the package is working before initial deployment, or when making changes 55 | > which would affect New Relic transactions. 56 | 57 | #### Live Environments 58 | Assuming the New Relic extension is loaded, the package sets up hooks into Laravel to monitor requests at different stages of the lifecycle: 59 | - **HTTP transactions** are handled with a global `NewRelicMiddleware` on each request 60 | - **CLI requests** are filtered out for noise (so long-running calls like `php artisan horizon` won't skew your stats). 61 | - **Queued jobs** record a transaction automatically as each one starts and ends. 62 | - **Artisan commands** are recorded as individual transactions. 63 | - **Scheduled tasks** are monitored as each one is executed. 64 | 65 | The package also registers a `php artisan new-relic:deploy` command, to notify New Relic of changes as part of your deployment process. 66 | 67 | ## Configuration 68 | 69 | The [configuration file](config/new-relic.php) is documented in detail — read through each comment to understand how it will affect transaction reporting. A few settings worth pointing out here are below: 70 | 71 | #### HTTP Requests 72 | ```php 73 | 'http' => [ 74 | 'middleware' => \JackWH\LaravelNewRelic\Middleware\NewRelicMiddleware::class, 75 | 76 | // ... 77 | 78 | 'rewrite' => [ 79 | '/livewire/livewire.js' => 'livewire.js', 80 | '/livewire/livewire.js.map' => 'livewire.js.map', 81 | ], 82 | 83 | // ... 84 | 85 | 'ignore' => [ 86 | 'debugbar.**', 87 | 'horizon.**', 88 | 'telescope.**', 89 | ], 90 | ], 91 | ``` 92 | 93 | The built-in `NewRelicMiddleware` class should be fine for most use cases, but you can extend it with your own implementation if needed. 94 | 95 | The `rewrite` key is useful for routes which don't have names defined (often the case with packages that expose public resources, like Livewire). You can rewrite their names for consistency here. 96 | 97 | We've set some sensible `ignore` rules by default, feel free to adjust as required. 98 | 99 | #### Queue Handling 100 | ```php 101 | 'queue' => [ 102 | 'ignore' => [ 103 | 'connections' => ['sync'], 104 | 'queues' => [], 105 | 'jobs' => [], 106 | ], 107 | ], 108 | ``` 109 | 110 | By default the `sync` connection will be ignored. This means a new job starting on this queue won't interrupt the existing transaction that started at the beginning of the request. You can also filter out specific queues and jobs, too. 111 | 112 | ## Deployments 113 | 114 | After each new deployment, you should notify New Relic so they can report on metric variances across multiple releases. The package includes a command to do this: 115 | 116 | ```php 117 | php artisan new-relic:deploy [description] [revision] 118 | ``` 119 | 120 | If you don't provide a git revision hash, the package can attempt to auto-detect it by calling `git log --pretty="%H" -n1 HEAD` 121 | 122 | --- 123 | 124 | ### To-Do 125 | 126 | 1. Improve the loggable transactions, make it clearer that HTTP transactions will end automatically 127 | 2. Add some tests 128 | 3. Hopefully someone can confirm if this works with Octane? 129 | 130 | ### Contributing 131 | 132 | All contributions are welcome! And if you found this useful, I'd love to know. 133 | 134 | ### Credits 135 | 136 | - [Jack Webb-Heller](https://github.com/JackWH) 137 | - [All Contributors](../../contributors) 138 | 139 | ### License 140 | 141 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 142 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jackwh/laravel-new-relic", 3 | "description": "Monitor your Laravel application performance with New Relic", 4 | "keywords": [ 5 | "JackWH", 6 | "laravel-new-relic", 7 | "laravel", 8 | "new relic", 9 | "performance", 10 | "monitoring" 11 | ], 12 | "homepage": "https://github.com/JackWH/laravel-new-relic", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Jack Webb-Heller", 17 | "email": "hello@jwh.fyi", 18 | "role": "Developer" 19 | } 20 | ], 21 | "require": { 22 | "php": "^8.1", 23 | "illuminate/contracts": "^9.0|^10.0|^11.0|^12.0", 24 | "illuminate/support": "^9.0|^10.0|^11.0|^12.0" 25 | }, 26 | "require-dev": { 27 | "driftingly/rector-laravel": "^1.2|^2.0", 28 | "larastan/larastan": "^2.9.0", 29 | "laravel/pint": "^1.15", 30 | "nunomaduro/collision": "^6.0|^8.0", 31 | "orchestra/testbench": "^7.0|^9.0|^10.0", 32 | "pestphp/pest": "^1.21|^2.34|^3.7", 33 | "pestphp/pest-plugin-laravel": "^1.1|^2.3|^3.1", 34 | "phpstan/extension-installer": "^1.1", 35 | "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", 36 | "phpstan/phpstan-phpunit": "^1.0|^2.0", 37 | "phpunit/phpunit": "^9.5|^10.5|^11.5.3", 38 | "rector/rector": "^1.0|^2.0", 39 | "roave/security-advisories": "dev-latest", 40 | "spatie/laravel-ray": "^1.26" 41 | }, 42 | "autoload": { 43 | "psr-4": { 44 | "JackWH\\LaravelNewRelic\\": "src" 45 | } 46 | }, 47 | "autoload-dev": { 48 | "psr-4": { 49 | "JackWH\\LaravelNewRelic\\Tests\\": "tests" 50 | } 51 | }, 52 | "scripts": { 53 | "analyse": "vendor/bin/phpstan analyse", 54 | "lint": "vendor/bin/pint", 55 | "test": "vendor/bin/pest", 56 | "test-coverage": "vendor/bin/pest --coverage" 57 | }, 58 | "config": { 59 | "sort-packages": true, 60 | "allow-plugins": { 61 | "pestphp/pest-plugin": true, 62 | "phpstan/extension-installer": true 63 | } 64 | }, 65 | "extra": { 66 | "laravel": { 67 | "providers": [ 68 | "JackWH\\LaravelNewRelic\\LaravelNewRelicServiceProvider" 69 | ], 70 | "aliases": { 71 | "NewRelicTransaction": "JackWH\\LaravelNewRelic\\Facades\\NewRelicTransaction" 72 | } 73 | } 74 | }, 75 | "minimum-stability": "dev", 76 | "prefer-stable": true 77 | } 78 | -------------------------------------------------------------------------------- /config/new-relic.php: -------------------------------------------------------------------------------- 1 | ini_get('newrelic.appname') ?: env('APP_NAME', 'laravel'), 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Environments 38 | |-------------------------------------------------------------------------- 39 | | 40 | | environments: Set which environments you'd expect New Relic to be 41 | | installed in. The package will only run in these 42 | | environments. 43 | | 44 | | loggable: When testing this package in one of these environments, 45 | | we can bootstrap the New Relic functions, and write logs 46 | | indicating what would be happening instead. Useful for 47 | | ensuring this package is set up properly before deploying. 48 | | Comment out 'local' after testing to avoid excessive logging. 49 | | 50 | */ 51 | 'environments' => [ 52 | 'production', 53 | ], 54 | 'loggable' => [ 55 | 'local', 56 | ], 57 | 58 | /* 59 | |-------------------------------------------------------------------------- 60 | | HTTP Requests 61 | |-------------------------------------------------------------------------- 62 | | 63 | | middleware: For HTTP requests, we'll apply the package NewRelicMiddleware 64 | | to create transactions in New Relic. If you want to customise 65 | | the default middleware, extend it and provide your custom 66 | | classname. Middleware will be applied automatically, but you 67 | | may want to set its priority in app/Http/Kernel.php's 68 | | $middlewarePriory array, to apply it as early in the stack 69 | | as possible. If you want to capture user attributes, it 70 | | must come *after* \Illuminate\Auth\Middleware\Authorize. 71 | | 72 | | visitors: If 'record_user_id' is true, we'll save the user's ID in the 73 | | transaction. If no user is logged in, the transaction will 74 | | show with as "Guest" (or your preferred 'guest_label'). Set 75 | | guest_label to null if you don't want to report this. You 76 | | can also optionally log the visitor's IP address too. 77 | | 78 | | rewrite: Specify any paths which should be given a custom transaction 79 | | name in New Relic. This is useful for when you have a route 80 | | which doesn't have a name (for example, in a third-party 81 | | package), and want to name it consistently without falling 82 | | back to the full controller class and action (see below). 83 | | 84 | | prefix: HTTP transactions will be identified in New Relic by the name 85 | | of their route, or if the route doesn't have a name, by their 86 | | controller action. Here you can also specify an optional 87 | | prefix to identify HTTP requests. For example, "Route " would 88 | | label HTTP requests in New Relic as "Route admin.dashboard". 89 | | 90 | | ignore: Set which HTTP requests should be ignored by New Relic. 91 | | 92 | */ 93 | 'http' => [ 94 | 'middleware' => \JackWH\LaravelNewRelic\Middleware\NewRelicMiddleware::class, 95 | 96 | 'visitors' => [ 97 | 'record_user_id' => true, 98 | 'record_ip_address' => false, 99 | 'guest_label' => 'Guest', 100 | ], 101 | 102 | 'rewrite' => [ 103 | '/livewire/livewire.js' => 'livewire.js', 104 | '/livewire/livewire.js.map' => 'livewire.js.map', 105 | ], 106 | 107 | 'prefix' => '', 108 | 109 | 'ignore' => [ 110 | 'debugbar.**', 111 | 'horizon.**', 112 | 'telescope.**', 113 | ], 114 | ], 115 | 116 | /* 117 | |-------------------------------------------------------------------------- 118 | | Artisan Commands 119 | |-------------------------------------------------------------------------- 120 | | 121 | | prefix: Artisan commands will be identified in New Relic by the base 122 | | command name (i.e. "php artisan migrate:rollback --force" will 123 | | appear as "migrate:rollback". You can specify an optional 124 | | prefix to label Artisan commands with, for example, "Artisan " 125 | | would label the command as "Artisan migrate:rollback". 126 | | 127 | | ignore: If you want to New Relic to ignore transactions for specific 128 | | Artisan commands, enter the command names here. Some sensible 129 | | defaults have been set already, to prevent New Relic skewing 130 | | runtime stats for background, noisy, or long-running processes. 131 | | 132 | */ 133 | 'artisan' => [ 134 | 'prefix' => '', 135 | 136 | 'ignore' => [ 137 | 'db', 138 | 'dusk', 139 | 'horizon', 140 | 'horizon:supervisor', 141 | 'horizon:work', 142 | 'queue:listen', 143 | 'queue:work', 144 | 'schedule:run', 145 | 'schedule:finish', 146 | 'schedule:work', 147 | 'serve', 148 | 'telescope:stream', 149 | 'test', 150 | 'tinker', 151 | ], 152 | ], 153 | 154 | /* 155 | |-------------------------------------------------------------------------- 156 | | Queue Handling 157 | |-------------------------------------------------------------------------- 158 | | 159 | | prefix: Queued jobs will be identified in New Relic by calling the 160 | | \Illuminate\Queue\Jobs\Job->resolveName() method, or if 161 | | unavailable, using Illuminate\Queue\Jobs\Job->getName(). 162 | | You can specify an optional prefix to label queued jobs with, 163 | | e.g "Queue " would label a job as "Queue App\Jobs\ExampleJob". 164 | | 165 | | ignore: If you want New Relic to ignore transactions for specific 166 | | connections, queue names, or job classes, enter them here. 167 | | 168 | */ 169 | 'queue' => [ 170 | 'prefix' => '', 171 | 172 | 'ignore' => [ 173 | 'connections' => ['sync'], 174 | 'queues' => [], 175 | 'jobs' => [], 176 | ], 177 | ], 178 | 179 | /* 180 | |-------------------------------------------------------------------------- 181 | | Scheduled Tasks 182 | |-------------------------------------------------------------------------- 183 | | 184 | | prefix: Scheduled tasks will be identified in New Relic by the task's 185 | | name, e.g. $schedule->command('db:backup')->name('Backup DB'). 186 | | If unavailable, the task's command name will be used instead. 187 | | You can specify an optional prefix to label scheduled tasks 188 | | with, e.g "Task " would label as "Task Backup DB" (or just 189 | | "Task db:backup" if no name has been set in the scheduler). 190 | | 191 | | ignore: If you want New Relic to ignore transactions for specific 192 | | scheduled tasks, enter the task names/descriptions here. 193 | | By using task names, closure-based tasks can be ignored too. 194 | | Use $schedule->command('...')->name('example') to set a name 195 | | for a task in app/Console/Kernel.php. Tasks will be ignored 196 | | if their name matches this configuration value, but also if 197 | | a scheduled task executes a command or job which was already 198 | | ignored in the Artisan or Queue configuration sections. 199 | | 200 | */ 201 | 'scheduler' => [ 202 | 'prefix' => '', 203 | 204 | 'ignore' => [ 205 | // 206 | ], 207 | ], 208 | 209 | /* 210 | |-------------------------------------------------------------------------- 211 | | Deployments 212 | |-------------------------------------------------------------------------- 213 | | 214 | | Provide a New Relic API key and your APM application ID to use the 215 | | Deployment Notification command. 216 | | 217 | | endpoint: If you have an EU account, you might need to change this to 218 | | https://api.eu.newrelic.com/v2/ (check your account) 219 | | 220 | | user: The user email address to log the deployment with. 221 | | 222 | | detect_hash: If true, and no git commit hash is passed to the command, 223 | | the package will attempt to auto-detect the revision. 224 | */ 225 | 'deployments' => [ 226 | 'api_key' => env('NEW_RELIC_API_KEY'), 227 | 'app_id' => env('NEW_RELIC_APP_ID'), 228 | 'endpoint' => env('NEW_RELIC_API_ENDPOINT', 'https://api.newrelic.com/v2/'), 229 | 230 | 'user' => 'you@example.com', 231 | 232 | 'detect_hash' => true, 233 | ], 234 | 235 | ]; 236 | -------------------------------------------------------------------------------- /resources/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackwh/laravel-new-relic/797a42c6345614eac64ae59d46497fb722bd74e8/resources/views/.gitkeep -------------------------------------------------------------------------------- /src/Commands/NewRelicDeployCommand.php: -------------------------------------------------------------------------------- 1 | argument('revision') ?: $this->detectRevision(); 31 | 32 | $nr = Http::withHeaders(['Api-Key' => config('new-relic.deployments.api_key')]) 33 | ->asJson() 34 | ->post(config('new-relic.deployments.endpoint') . 'applications/' . config('new-relic.deployments.app_id') . '/deployments.json', [ 35 | 'deployment' => [ 36 | 'revision' => $revision, 37 | 'description' => $this->argument('description'), 38 | 'user' => config('new-relic.deployments.user'), 39 | ], 40 | ]); 41 | 42 | if ($nr->successful()) { 43 | $this->info('Notified New Relic!'); 44 | 45 | return parent::SUCCESS; 46 | } 47 | 48 | $this->warn('Could not notify New Relic [HTTP ' . $nr->status() . ']'); 49 | $this->warn('See: https://rpm.eu.newrelic.com/api/explore/application_deployments/create'); 50 | 51 | return parent::FAILURE; 52 | } 53 | 54 | /** 55 | * Attempt to auto-detect the current git revision hash. 56 | */ 57 | public function detectRevision(): ?string 58 | { 59 | if (! config('new-relic.deployments.detect_hash')) { 60 | return null; 61 | } 62 | 63 | try { 64 | return trim(exec('git log --pretty="%H" -n1 HEAD')); 65 | } catch (Throwable $throwable) { 66 | $this->warn('Could not auto-detect revision hash: ' . $throwable->getMessage()); 67 | } 68 | 69 | return null; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Facades/NewRelicTransaction.php: -------------------------------------------------------------------------------- 1 | debug( 25 | 'New Relic: transaction is now named "' 26 | . app(NewRelicTransaction::class)->identifier(false) 27 | . '", with object ID ' . spl_object_id(app(NewRelicTransaction::class)) 28 | ); 29 | } 30 | } 31 | 32 | if (! function_exists('newrelic_add_custom_parameter')) { 33 | /** 34 | * Add a custom parameter for New Relic. 35 | */ 36 | function newrelic_add_custom_parameter(string $key, int|float|string $value): void 37 | { 38 | app('log')->debug( 39 | 'New Relic: ' 40 | . app(NewRelicTransaction::class)->identifier() 41 | . ' set custom parameter "' . $key . '" to "' . $value . '"', 42 | ); 43 | } 44 | } 45 | 46 | if (! function_exists('newrelic_background_job')) { 47 | /** 48 | * Tell New Relic this transaction is a background job. 49 | */ 50 | function newrelic_background_job(): void 51 | { 52 | app('log')->debug( 53 | 'New Relic: ' 54 | . app(NewRelicTransaction::class)->identifier() 55 | . ' is a background job.' 56 | ); 57 | } 58 | } 59 | 60 | if (! function_exists('newrelic_ignore_transaction')) { 61 | /** 62 | * Tell New Relic to ignore the current transaction. 63 | */ 64 | function newrelic_ignore_transaction(): void 65 | { 66 | app('log')->debug( 67 | 'New Relic: ' 68 | . app(NewRelicTransaction::class)->identifier() 69 | . ' ignored.' 70 | ); 71 | } 72 | } 73 | 74 | if (! function_exists('newrelic_start_transaction')) { 75 | /** 76 | * Tell New Relic to start the current transaction. 77 | */ 78 | function newrelic_start_transaction(string $appName): void 79 | { 80 | app('log')->debug( 81 | 'New Relic: ' 82 | . app(NewRelicTransaction::class)->identifier() 83 | . ' started for application "' . $appName . '"' 84 | ); 85 | } 86 | } 87 | 88 | if (! function_exists('newrelic_end_transaction')) { 89 | /** 90 | * Tell New Relic to end the current transaction. 91 | */ 92 | function newrelic_end_transaction(): void 93 | { 94 | app('log')->debug( 95 | 'New Relic: ' . 96 | app(NewRelicTransaction::class)->identifier() 97 | . ' ended.' 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/LaravelNewRelicServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom( 19 | __DIR__ . '/../config/new-relic.php', 20 | 'new-relic' 21 | ); 22 | 23 | // Bind the transaction and handler classes to the container. 24 | // We bind them as scoped singletons, meaning they will be 25 | // automatically reset at the end of each lifecycle request. 26 | $this->app->scoped(NewRelicTransaction::class, static fn ($app): NewRelicTransaction => new NewRelicTransaction()); 27 | $this->app->scoped(NewRelicTransactionHandler::class, static fn ($app): NewRelicTransactionHandler => new NewRelicTransactionHandler()); 28 | } 29 | 30 | /** 31 | * Boot the New Relic Service Provider. 32 | */ 33 | public function boot(): void 34 | { 35 | $this->publishes([ 36 | __DIR__.'/../config/new-relic.php' => config_path('new-relic.php'), 37 | ]); 38 | 39 | if ($this->app->runningInConsole()) { 40 | // Register the new-relic:deploy command in the console 41 | $this->commands([NewRelicDeployCommand::class]); 42 | } 43 | 44 | if (app(NewRelicTransactionHandler::class)::newRelicEnabled()) { 45 | app(NewRelicTransactionHandler::class)->configureNewRelic(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Middleware/NewRelicMiddleware.php: -------------------------------------------------------------------------------- 1 | setName($this->requestName($request)) 34 | // Record the IP address, if configured. 35 | ->addParameter( 36 | 'ip_address', 37 | config('new-relic.http.visitors.record_ip_address') ? $request->ip() : null 38 | ); 39 | 40 | // Tell the application to handle the incoming request before continuing... 41 | $response = $next($request); 42 | 43 | // Skip further New Relic configuration if required. 44 | if ((request()->is($this->ignoredRoutes())) 45 | || (request()->routeIs($this->ignoredRoutes())) 46 | || (request()->fullUrlIs($this->ignoredRoutes()))) { 47 | app(NewRelicTransaction::class)->ignore(); 48 | 49 | return $response; 50 | } 51 | 52 | // With the response now prepared, we can access the authenticated user. 53 | if (config('new-relic.http.visitors.record_user_id')) { 54 | $this->user = Auth::user(); 55 | } 56 | 57 | // Add custom parameters to the transaction. 58 | app(NewRelicTransaction::class) 59 | ->addParameter( 60 | 'user_type', 61 | $this->user instanceof Authenticatable ? 'User' : config('new-relic.http.visitors.guest_label') 62 | )->addParameter( 63 | 'user_id', 64 | $this->user?->getAuthIdentifier(), 65 | ); 66 | 67 | // If the request name resolves differently, update it. 68 | app(NewRelicTransaction::class)->setName($this->requestName($request)); 69 | 70 | // Return the previous response and continue. 71 | return $response; 72 | } 73 | 74 | /** 75 | * Get the name of the current request. This may change during the request lifecycle, 76 | * so this method is called twice, both before and after the request is handled. 77 | */ 78 | protected function requestName(Request $request): string 79 | { 80 | return config('new-relic.http.prefix') . ( 81 | $this->getCustomTransactionName($request) 82 | ?? $this->getLivewireTransactionName($request) 83 | ?? $request->route()?->getName() 84 | ?? $request->route()?->getActionName() 85 | ?? $request->path() 86 | ); 87 | } 88 | 89 | /** 90 | * An array of routes where this middleware shouldn't be applied. 91 | */ 92 | protected function ignoredRoutes(): array 93 | { 94 | return array_merge(config('new-relic.http.ignore'), [ 95 | // 96 | ]); 97 | } 98 | 99 | /** 100 | * Rewrite any custom transaction names, by path => name. 101 | */ 102 | protected function mapCustomTransactionNames(): array 103 | { 104 | return array_merge(config('new-relic.http.rewrite'), [ 105 | // 106 | ]); 107 | } 108 | 109 | /** 110 | * Get a custom name for a transaction by the currently-requested URI. 111 | */ 112 | protected function getCustomTransactionName(Request $request): ?string 113 | { 114 | return collect($this->mapCustomTransactionNames()) 115 | ->mapWithKeys(static fn (string $name, string $path): array => [ 116 | (Str::of($path)->trim('/')->toString() ?: '/') => $name, 117 | ])->get( 118 | Str::of($request->path())->trim('/')->toString() ?: '/' 119 | ); 120 | } 121 | 122 | /** 123 | * If the current request is to Livewire's messaging endpoint, set a custom name from the component. 124 | */ 125 | protected function getLivewireTransactionName(Request $request): ?string 126 | { 127 | if (! $request->routeIs('livewire.message')) { 128 | return null; 129 | } 130 | 131 | return 'livewire.' . $request->route()->parameter('name', 'message'); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/NewRelicTransaction.php: -------------------------------------------------------------------------------- 1 | isBackground = app()->runningInConsole(); 28 | } 29 | 30 | /** 31 | * Tell New Relic the transaction is running in the CLI. 32 | */ 33 | public function background(): self 34 | { 35 | if ($this->isIgnored()) { 36 | return $this; 37 | } 38 | 39 | newrelic_background_job(); 40 | 41 | $this->isBackground = true; 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * Ignore this transaction from further reporting. 48 | */ 49 | public function ignore(): self 50 | { 51 | if (!$this->isIgnored()) { 52 | newrelic_ignore_transaction(); 53 | 54 | $this->name = 'transaction'; 55 | $this->isIgnored = true; 56 | $this->isActive = false; 57 | } 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * Check if the transaction is being ignored. 64 | */ 65 | public function isIgnored(): bool 66 | { 67 | return $this->isIgnored; 68 | } 69 | 70 | /** 71 | * Set the name for this transaction. 72 | */ 73 | public function setName(string $name): self 74 | { 75 | $name = trim($name, '/ '); 76 | 77 | if ($this->name === $name || $this->isActive($name)) { 78 | return $this; 79 | } 80 | 81 | $this->name = $name; 82 | newrelic_name_transaction($this->name); 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * Add a custom parameter to the transaction. 89 | */ 90 | public function addParameter( 91 | string $key, 92 | int|float|string|null $value 93 | ): self { 94 | if ($this->isIgnored()) { 95 | return $this; 96 | } 97 | 98 | if ($value !== null && (!$this->hasParameter($key, $value))) { 99 | newrelic_add_custom_parameter($key, $value); 100 | $this->parameters[$key] = $value; 101 | } 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * Check if the transaction has a specific parameter set. 108 | */ 109 | public function hasParameter( 110 | string $key, 111 | int|float|string|null $value 112 | ): bool { 113 | return array_key_exists($key, $this->parameters) 114 | && ($value === null || $this->parameters[$key] === $value); 115 | } 116 | 117 | /** 118 | * Start the transaction with a given name. 119 | */ 120 | public function start(string $name): self 121 | { 122 | // If the same transaction is already active, continue. 123 | if ($this->isActive($name)) { 124 | return $this; 125 | } 126 | 127 | $this->isIgnored = false; 128 | $this->isActive = true; 129 | 130 | $this->end(); 131 | newrelic_start_transaction(config('new-relic.app_name')); 132 | $this->setName($name); 133 | 134 | return $this; 135 | } 136 | 137 | /** 138 | * End the transaction, and reset it back to its default state. 139 | * Specify $ifNamed to end it only if it has a specific name. 140 | */ 141 | public function end(?string $ifNamed = null): self 142 | { 143 | if ($this->isActive($ifNamed)) { 144 | newrelic_end_transaction(); 145 | 146 | $this->name = 'transaction'; 147 | $this->parameters = []; 148 | $this->isBackground = app()->runningInConsole(); 149 | $this->isActive = false; 150 | $this->isIgnored = false; 151 | } 152 | 153 | return $this; 154 | } 155 | 156 | /** 157 | * Check if the transaction is active, optionally filtered to a specific 158 | * name. 159 | */ 160 | public function isActive(?string $withName = null): bool 161 | { 162 | return $this->isActive 163 | && (!$this->isIgnored) 164 | && ($withName === null || $this->name === $withName); 165 | } 166 | 167 | /** 168 | * Get a unique identifier for this NewRelicTransaction instance. 169 | * This is used in loggable environments, to give an insight into 170 | * which transactions are being started/stopped at a given moment. 171 | */ 172 | public function identifier(bool $withObjId = true): string 173 | { 174 | return $this->name . ($withObjId ? ' [' . spl_object_id( 175 | $this 176 | ) . ']' : ''); 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /src/NewRelicTransactionHandler.php: -------------------------------------------------------------------------------- 1 | environment(config('new-relic.loggable'))) { 33 | require_once(__DIR__ . '/Helpers/LoggableNewRelicFunctions.php'); 34 | 35 | return true; 36 | } 37 | 38 | return extension_loaded('newrelic') 39 | && app()->environment(config('new-relic.environments')); 40 | } 41 | 42 | /** 43 | * Configure New Relic to handle different types of Laravel requests and 44 | * actions. 45 | */ 46 | public function configureNewRelic(): void 47 | { 48 | // Set up New Relic within a try/catch, so that if there's any misconfiguration, 49 | // we won't risk unexpectedly taking down a production server. 50 | try { 51 | $this->httpRequests(); 52 | $this->cliRequests(); 53 | $this->queueHandling(); 54 | $this->artisanCommands(); 55 | $this->scheduledTasks(); 56 | } catch (\Throwable $throwable) { 57 | if (app()->environment(config('new-relic.loggable'))) { 58 | throw $throwable; 59 | } 60 | 61 | Log::error( 62 | 'Error configuring New Relic: ' . $throwable->getMessage(), 63 | $throwable->getTrace() 64 | ); 65 | } 66 | } 67 | 68 | /** 69 | * Configure New Relic for CLI requests to the application. 70 | */ 71 | public function cliRequests(): void 72 | { 73 | if (!app()->runningInConsole()) { 74 | return; 75 | } 76 | 77 | // Apply our own early determination of the transaction name, 78 | // and tell New Relic this is a background job. 79 | app(NewRelicTransaction::class) 80 | ->setName(Str::before($this->getCommandString(), ' ')) 81 | ->addParameter( 82 | 'command', 83 | collect($this->getCommandArgs())->implode(' ') 84 | ) 85 | ->background(); 86 | } 87 | 88 | /** 89 | * Configure New Relic for HTTP requests to the application. 90 | */ 91 | public function httpRequests(): void 92 | { 93 | if (app()->runningInConsole()) { 94 | return; 95 | } 96 | 97 | // Register the NewRelicMiddleware for HTTP requests 98 | if ($middleware = config('new-relic.http.middleware')) { 99 | app(Kernel::class)->pushMiddleware($middleware); 100 | } 101 | } 102 | 103 | /** 104 | * Configure New Relic for Queue handling. 105 | */ 106 | public function queueHandling(): void 107 | { 108 | /** 109 | * Before each job begins processing, start a new transaction. 110 | */ 111 | app('queue')->before( 112 | function (JobProcessing $jobProcessing): void { 113 | if ($this->shouldIgnoreJob( 114 | $jobProcessing->connectionName, 115 | $jobProcessing->job->getQueue(), 116 | $jobProcessing->job 117 | )) { 118 | app(NewRelicTransaction::class)->ignore(); 119 | 120 | return; 121 | } 122 | 123 | // Start a new transaction for this job 124 | app(NewRelicTransaction::class) 125 | ->start( 126 | config('new-relic.queue.prefix') . 127 | ((method_exists($jobProcessing->job, 'resolveName')) 128 | ? $jobProcessing->job->resolveName() 129 | : $jobProcessing->job->getName()) 130 | )->addParameter('queue', $jobProcessing->job->getQueue()) 131 | ->addParameter( 132 | 'connection', 133 | $jobProcessing->connectionName 134 | ); 135 | } 136 | ); 137 | 138 | /** 139 | * After each job finishes processing, end the previous transaction. 140 | */ 141 | app('queue')->after( 142 | static function (/*JobProcessed $jobProcessed*/): void { 143 | app(NewRelicTransaction::class)->end(); 144 | } 145 | ); 146 | } 147 | 148 | /** 149 | * Determine whether a queue connection, queue name, or job should be 150 | * ignored. 151 | */ 152 | public function shouldIgnoreJob( 153 | ?string $connection = null, 154 | ?string $queue = null, 155 | ?Job $job = null, 156 | ): bool { 157 | if ($connection !== null && Str::is( 158 | config('new-relic.queue.ignore.connections'), 159 | $connection 160 | )) { 161 | return true; 162 | } 163 | 164 | if ($queue !== null && Str::is( 165 | config('new-relic.queue.ignore.queues'), 166 | $queue 167 | )) { 168 | return true; 169 | } 170 | 171 | return $job instanceof Job && Str::is( 172 | config('new-relic.queue.ignore.jobs'), 173 | $job::class 174 | ); 175 | } 176 | 177 | /** 178 | * Configure New Relic for Artisan commands made to the application. 179 | */ 180 | public function artisanCommands(): void 181 | { 182 | /** 183 | * When an Artisan command starts executing, begin a New Relic transaction. 184 | */ 185 | app('events')->listen( 186 | CommandStarting::class, 187 | function (CommandStarting $commandStarting): void { 188 | if ($this->shouldIgnoreCommand($commandStarting->command)) { 189 | app(NewRelicTransaction::class)->ignore(); 190 | 191 | return; 192 | } 193 | 194 | // End any previous transactions, as long as we're not still running in the same one, 195 | // then start a new transaction for this command. 196 | app(NewRelicTransaction::class)->start( 197 | config( 198 | 'new-relic.artisan.prefix' 199 | ) . $commandStarting->command 200 | )->addParameter( 201 | 'command', 202 | collect($this->getCommandArgs())->implode(' ') 203 | ); 204 | } 205 | ); 206 | 207 | /** 208 | * When a command finishes executing, end the transaction with New Relic. 209 | */ 210 | app('events')->listen( 211 | CommandFinished::class, 212 | static function (/*CommandFinished $commandFinished*/): void { 213 | app(NewRelicTransaction::class)->end(); 214 | } 215 | ); 216 | } 217 | 218 | /** 219 | * Determine whether an Artisan command should be ignored. 220 | */ 221 | public function shouldIgnoreCommand(?string $command = null): bool 222 | { 223 | return $command !== null && Str::is( 224 | config('new-relic.artisan.ignore'), 225 | $command 226 | ); 227 | } 228 | 229 | /** 230 | * Configure New Relic for scheduled tasks made by the application. 231 | */ 232 | public function scheduledTasks(): void 233 | { 234 | /** 235 | * When a scheduled task starts executing, begin a New Relic transaction. 236 | */ 237 | app('events')->listen( 238 | ScheduledTaskStarting::class, 239 | function (ScheduledTaskStarting $scheduledTaskStarting): void { 240 | if ($this->shouldIgnoreTask( 241 | $scheduledTaskStarting->task->description ?: $scheduledTaskStarting->task->command 242 | )) { 243 | app(NewRelicTransaction::class)->ignore(); 244 | 245 | return; 246 | } 247 | 248 | // End any previous transactions, as long as we're not still running in the same one, 249 | // then start a new transaction for this task. 250 | app(NewRelicTransaction::class)->start( 251 | config('new-relic.scheduler.prefix') . 252 | ($scheduledTaskStarting->task->description ?: $this->parseTaskCommand( 253 | $scheduledTaskStarting->task->command ?? 'unknown' 254 | )) 255 | )->addParameter( 256 | 'command', 257 | collect($this->getCommandArgs())->implode(' ') 258 | ); 259 | } 260 | ); 261 | 262 | /** 263 | * When a scheduled task finishes, end the transaction with New Relic. 264 | */ 265 | app('events')->listen( 266 | ScheduledTaskFinished::class, 267 | static function (/*ScheduledTaskFinished $scheduledTaskFinished*/): void { 268 | app(NewRelicTransaction::class)->end(); 269 | } 270 | ); 271 | app('events')->listen( 272 | ScheduledBackgroundTaskFinished::class, 273 | static function ( 274 | /*ScheduledBackgroundTaskFinished $scheduledBackgroundTaskFinished*/ 275 | ): void { 276 | app(NewRelicTransaction::class)->end(); 277 | } 278 | ); 279 | } 280 | 281 | /** 282 | * Determine whether a scheduled task should be ignored. 283 | */ 284 | public function shouldIgnoreTask(?string $task = null): bool 285 | { 286 | return $task !== null && Str::is( 287 | config('new-relic.scheduler.ignore'), 288 | $task 289 | ); 290 | } 291 | 292 | /** 293 | * Get any command arguments passed in to the current request. 294 | */ 295 | protected function getCommandArgs(): array 296 | { 297 | return collect(request()->server()) 298 | ->only('argv') 299 | ->flatten() 300 | ->toArray(); 301 | } 302 | 303 | /** 304 | * Get any command arguments passed in to the current request, as a 305 | * formatted string. 306 | */ 307 | protected function getCommandString(): string 308 | { 309 | $cmdName = trim( 310 | collect($this->getCommandArgs())->map( 311 | static fn (string $argument): string => Str::contains( 312 | $argument, 313 | '=' 314 | ) 315 | ? (Str::before($argument, '=') . '=?') 316 | : $argument 317 | )->implode(' ') 318 | ); 319 | 320 | return ltrim( 321 | Str::remove([base_path(), 'artisan '], $cmdName, false), 322 | '/ ' 323 | ); 324 | } 325 | 326 | /** 327 | * Parse a command string for a scheduled task, returning it in a 328 | * simplified format. 329 | */ 330 | protected function parseTaskCommand(string $taskCommand): string 331 | { 332 | $stringable = Str::of($taskCommand) 333 | ->afterLast("'artisan' ") 334 | ->afterLast("artisan ") 335 | ->trim("'/ "); 336 | 337 | return $stringable->contains(' ') 338 | ? $stringable->before(' ')->toString() 339 | : $stringable->toString(); 340 | } 341 | 342 | } 343 | --------------------------------------------------------------------------------