├── .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 | [](https://packagist.org/packages/jackwh/laravel-new-relic)
4 | [](https://github.com/jackwh/laravel-new-relic/actions?query=workflow%3Arun-tests+branch%3Amain)
5 | [](https://github.com/jackwh/laravel-new-relic/actions?query=workflow%3A"Check+%26+fix+styling"+branch%3Amain)
6 | [](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 |
--------------------------------------------------------------------------------