Repository: ekino/EkinoNewRelicBundle
Branch: master
Commit: aa29fd8e1f20
Files: 55
Total size: 168.9 KB
Directory structure:
gitextract_cqkhw3dv/
├── .editorconfig
├── .gitattributes
├── .github/
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .php-cs-fixer.php
├── CHANGELOG.md
├── Command/
│ └── NotifyDeploymentCommand.php
├── DependencyInjection/
│ ├── Compiler/
│ │ └── MonologHandlerPass.php
│ ├── Configuration.php
│ └── EkinoNewRelicExtension.php
├── EkinoNewRelicBundle.php
├── Exception/
│ └── DeprecationException.php
├── LICENSE
├── Listener/
│ ├── CommandListener.php
│ ├── DeprecationListener.php
│ ├── ExceptionListener.php
│ ├── RequestListener.php
│ └── ResponseListener.php
├── Logging/
│ └── AdaptiveHandler.php
├── NewRelic/
│ ├── AdaptiveInteractor.php
│ ├── BlackholeInteractor.php
│ ├── Config.php
│ ├── LoggingInteractorDecorator.php
│ ├── NewRelicInteractor.php
│ └── NewRelicInteractorInterface.php
├── README.md
├── Resources/
│ ├── config/
│ │ ├── command_listener.xml
│ │ ├── deprecation_listener.xml
│ │ ├── exception_listener.xml
│ │ ├── http_listener.xml
│ │ ├── monolog.xml
│ │ ├── services.xml
│ │ └── twig.xml
│ └── recipes/
│ └── newrelic.rb
├── Tests/
│ ├── AppKernel.php
│ ├── BundleInitializationTest.php
│ ├── DependencyInjection/
│ │ ├── Compiler/
│ │ │ └── MonologHandlerPassTest.php
│ │ ├── ConfigurationTest.php
│ │ └── EkinoNewRelicExtensionTest.php
│ ├── Listener/
│ │ ├── CommandListenerTest.php
│ │ ├── DeprecationListenerTest.php
│ │ ├── ExceptionListenerTest.php
│ │ ├── RequestListenerTest.php
│ │ └── ResponseListenerTest.php
│ ├── NewRelic/
│ │ ├── ConfigTest.php
│ │ └── LoggingInteractorDecoratorTest.php
│ ├── TransactionNamingStrategy/
│ │ └── ControllerNamingStrategyTest.php
│ └── Twig/
│ └── NewRelicExtensionTest.php
├── TransactionNamingStrategy/
│ ├── ControllerNamingStrategy.php
│ ├── RouteNamingStrategy.php
│ └── TransactionNamingStrategyInterface.php
├── Twig/
│ └── NewRelicExtension.php
├── UPGRADE-2.0.md
├── composer.json
└── phpunit.xml.dist
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
================================================
FILE: .gitattributes
================================================
.editorconfig export-ignore
.gitattributes export-ignore
.gitignore export-ignore
/.php_cs export-ignore
/.scrutinizer.yml export-ignore
/.styleci.yml export-ignore
/.travis.yml export-ignore
/phpunit.xml.dist export-ignore
/Tests/ export-ignore
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
pull_request: ~
push:
branches:
- master
jobs:
php-cs-fixer:
name: PHP-CS-Fixer
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: PHP-CS-Fixer
uses: docker://oskarstark/php-cs-fixer-ga
build:
name: Build
runs-on: Ubuntu-20.04
strategy:
fail-fast: false
matrix:
php: ['7.1', '7.2', '7.3', '7.4', '8.0']
composer: ['']
phpunit: ['']
deprecation: ['']
symfony: ['']
stability: ['']
include:
# Minimum supported dependencies with the latest and oldest PHP version
- php: 8.0
composer: --prefer-stable --prefer-lowest
deprecation: max[direct]=0
- php: 7.1
composer: --prefer-stable --prefer-lowest
deprecation: max[direct]=0
# symfony version
- php: 8.0
symfony: '^3.0'
- php: 8.0
symfony: '^4.0'
- php: 8.0
symfony: '^5.0'
# dev
- php: 8.0
stability: 'dev'
steps:
- name: Set up PHP
uses: shivammathur/setup-php@2.7.0
with:
php-version: ${{ matrix.php }}
coverage: none
tools: flex
- name: Checkout code
uses: actions/checkout@v2
- name: Setup stability
if: matrix.stability != ''
run: composer config minimum-stability "${{ matrix.stability }}"
- name: Setup deprecation
if: matrix.deprecation != ''
run: echo 'SYMFONY_DEPRECATIONS_HELPER=${{ matrix.deprecation }}' >> $GITHUB_ENV
- name: Setup symfony
if: matrix.symfony != ''
run: |
echo 'SYMFONY_REQUIRE=${{ matrix.symfony }}' >> $GITHUB_ENV
- name: Download dependencies
run: |
composer update ${{ matrix.composer}} --prefer-dist --no-interaction
./vendor/bin/simple-phpunit install
- name: Validate
run: |
composer validate --strict --no-check-lock
- name: Run tests
env:
SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT: 1
run: |
${{ matrix.phpunit }} ./vendor/bin/simple-phpunit
================================================
FILE: .gitignore
================================================
.php_cs.cache
build
phpunit.xml
coverage
composer.lock
vendor
================================================
FILE: .php-cs-fixer.php
================================================
<?php
$header = <<<'EOF'
This file is part of Ekino New Relic bundle.
(c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
EOF;
return (new PhpCsFixer\Config())
->setRiskyAllowed(true)
->setRules([
'@Symfony' => true,
'@Symfony:risky' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => true,
'header_comment' => ['header' => $header],
'linebreak_after_opening_tag' => true,
'modernize_types_casting' => true,
'no_superfluous_elseif' => true,
'no_useless_else' => true,
'phpdoc_order' => true,
'psr_autoloading' => true,
'simplified_null_return' => true,
'php_unit_strict' => true,
'no_useless_return' => true,
'strict_param' => true,
'strict_comparison' => true,
'yoda_style' => true,
'declare_strict_types' => true,
'native_function_invocation' => true,
])
->setFinder(
PhpCsFixer\Finder::create()
->in(__DIR__)
->exclude('vendor')
->name('*.php')
)
->setCacheFile(__DIR__.'/.php_cs.cache')
->setUsingCache(true)
;
================================================
FILE: CHANGELOG.md
================================================
# Changelog
## v2.4.0
### Added
- Support for distributed tracing functions provided by the NewRelic PHP extension are now supported in the `NewRelicInteractorInterface`
## v2.3.0
### Added
- Make recordDatastoreSegment execute the callback
## v2.2.3
### Added
- Allow Symfony 6.0
- Fix Symfony 5.3 deprecations
## v2.2.2
### Added
- Test against PHP 8.0
## v2.2.1
### Fixed
- Fixed class loaded twice.
## v2.2.0
### Added
- Added `api_host` configuration property used by `NotifyDeploymentCommand`
## v2.1.3
### Added
- Test against PHP 7.4
- Use typehinted alias in EventListener
### Fixed
- Wrong event handled in RequestListener
## v2.1.2
### Fixed
- Fixed compatibility issues with Symfony 5.0
- Handle new ResponseEvent, RequestEvent and ExceptionEvent in EventListeners
## v2.1.1
### Added
- Allow Symfony 5.0
## v2.1.0
### Added
- More detail/context when PSR-3 Logging the Newrelic transactions
### Fixed
- Even when handling a streamed response should call 'endTransaction' on onKernelResponse even
- Warnings in PHP 7.4
- Stop using Twig deprecated classes
## v2.0.2
### Changed
- Remove deprecations triggered by Symfony 4.0.
- Excluded tests from classmap.
### Fixed
- Fixed call to non-allowed method `setContent` on a `StreamedResponse`.
- Fixed multiple decoration of error handler when the bundle is often started and stopped like in test suite.
- Fixed issue in monolog's service configuration that does not allows application's services or aliases.
## v2.0.1
### Fixed
- Fixed type error when configuration's property `deployment_names` is not a string
## v2.0.0
### Changed
- Update the return type annotation of `NewRelicInteractorInterface::disableAutoRUM` to `?bool`
to match the latest changes in the NewRelic API.
## v2.0.0-beta5
### Fixed
- Memory leak in the `ResponseListener` that may cause issues on large HTML responses.
- Fixed type error when no Content-Type header was returned.
- Make sure `NewRelicInteractor::disableAutoRUM` always returns true.
## v2.0.0-beta4
### Changed
- Changed the configuration for monolog's channel to a configuration similar to MonologBundle.
## v2.0.0-beta3
### Changed
- Moved "instrument" to the root level
- The `AdaptiveInteractor` is now the default interactor.
### Fixed
- Bug where logging deprecations did not work.
## v2.0.0-beta2
### Changed
- Add default "deployment_names"
- Updated interface variable names to match the NewRelic extension.
## v2.0.0-beta1
### Added
- All functions provided by the NewRelic PHP extension are now supported in the `NewRelicInteractorInterface`.
- Added a new `deprecations` parameter to logs `E_USER_DEPRECATED`.
- Added a new `monolog` parameter to send logs to new relic.
### Changed
- Command Configuration explicit
- The configuration syntax
- The bundle uses class-named service ids. See `UPGRADE-2.0.md` for the exhaustive list of changes
### Removed
- Support for Silex
- Support for PHP < 7.1
- Support for Symfony < 3.4
================================================
FILE: Command/NotifyDeploymentCommand.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Command;
use Ekino\NewRelicBundle\NewRelic\Config;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class NotifyDeploymentCommand extends Command
{
public const EXIT_NO_APP_NAMES = 1;
public const EXIT_UNAUTHORIZED = 2;
public const EXIT_HTTP_ERROR = 3;
protected static $defaultName = 'newrelic:notify-deployment';
private $newrelic;
public function __construct(Config $newrelic)
{
$this->newrelic = $newrelic;
parent::__construct();
}
protected function configure(): void
{
$this
->setDefinition([
new InputOption(
'user', null, InputOption::VALUE_OPTIONAL,
'The name of the user/process that triggered this deployment', null
),
new InputOption(
'revision', null, InputOption::VALUE_OPTIONAL,
'A revision number (e.g., git commit SHA)', null
),
new InputOption(
'changelog', null, InputOption::VALUE_OPTIONAL,
'A list of changes for this deployment', null
),
new InputOption(
'description', null, InputOption::VALUE_OPTIONAL,
'Text annotation for the deployment — notes for you', null
),
])
->setDescription('Notifies New Relic that a new deployment has been made')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$appNames = $this->newrelic->getDeploymentNames();
if (!$appNames) {
$output->writeLn('<error>No deployment application configured.</error>');
return self::EXIT_NO_APP_NAMES;
}
$exitCode = 0;
foreach ($appNames as $appName) {
$response = $this->performRequest($this->newrelic->getApiKey(), $this->createPayload($appName, $input), $this->newrelic->getApiHost());
switch ($response['status']) {
case 200:
case 201:
$output->writeLn(sprintf("Recorded deployment to '%s' (%s)", $appName, ($input->getOption('description') ?: date('r'))));
break;
case 403:
$output->writeLn(sprintf("<error>Deployment not recorded to '%s': API key invalid</error>", $appName));
$exitCode = self::EXIT_UNAUTHORIZED;
break;
case null:
$output->writeLn(sprintf("<error>Deployment not recorded to '%s': Did not understand response</error>", $appName));
$exitCode = self::EXIT_HTTP_ERROR;
break;
default:
$output->writeLn(sprintf("<error>Deployment not recorded to '%s': Received HTTP status %d</error>", $appName, $response['status']));
$exitCode = self::EXIT_HTTP_ERROR;
break;
}
}
return $exitCode;
}
public function performRequest(string $api_key, string $payload, ?string $api_host = null): array
{
$headers = [
sprintf('x-api-key: %s', $api_key),
'Content-type: application/x-www-form-urlencoded',
];
$context = [
'http' => [
'method' => 'POST',
'header' => implode("\r\n", $headers),
'content' => $payload,
'ignore_errors' => true,
],
];
$level = error_reporting(0);
$content = file_get_contents(sprintf('https://%s/deployments.xml', $api_host ?? 'api.newrelic.com'), false, stream_context_create($context));
error_reporting($level);
if (false === $content) {
$error = error_get_last();
throw new \RuntimeException($error['message']);
}
$response = [
'status' => null,
'error' => null,
];
if (isset($http_response_header[0])) {
preg_match('/^HTTP\/1.\d (\d+)/', $http_response_header[0], $matches);
if (isset($matches[1])) {
$status = $matches[1];
$response['status'] = $status;
preg_match('/<error>(.*?)<\/error>/', $content, $matches);
if (isset($matches[1])) {
$response['error'] = $matches[1];
}
}
}
return $response;
}
private function createPayload(string $appName, InputInterface $input): string
{
$content_array = [
'deployment[app_name]' => $appName,
];
if (($user = $input->getOption('user'))) {
$content_array['deployment[user]'] = $user;
}
if (($revision = $input->getOption('revision'))) {
$content_array['deployment[revision]'] = $revision;
}
if (($changelog = $input->getOption('changelog'))) {
$content_array['deployment[changelog]'] = $changelog;
}
if (($description = $input->getOption('description'))) {
$content_array['deployment[description]'] = $description;
}
return http_build_query($content_array);
}
}
================================================
FILE: DependencyInjection/Compiler/MonologHandlerPass.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
class MonologHandlerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->hasParameter('ekino.new_relic.monolog') || !$container->hasDefinition('monolog.logger')) {
return;
}
$configuration = $container->getParameter('ekino.new_relic.monolog');
if ($container->hasDefinition('ekino.new_relic.logs_handler') && $container->hasParameter('ekino.new_relic.application_name')) {
$container->findDefinition('ekino.new_relic.logs_handler')
->setArgument('$level', \is_int($configuration['level']) ? $configuration['level'] : \constant('Monolog\Logger::'.strtoupper($configuration['level'])))
->setArgument('$bubble', true)
->setArgument('$appName', $container->getParameter('ekino.new_relic.application_name'));
}
if (!isset($configuration['channels'])) {
$channels = $this->getChannels($container);
} elseif ('inclusive' === $configuration['channels']['type']) {
$channels = $configuration['channels']['elements'] ?: $this->getChannels($container);
} else {
$channels = array_diff($this->getChannels($container), $configuration['channels']['elements']);
}
foreach ($channels as $channel) {
try {
$def = $container->getDefinition('app' === $channel ? 'monolog.logger' : 'monolog.logger.'.$channel);
} catch (InvalidArgumentException $e) {
$msg = 'NewRelicBundle configuration error: The logging channel "'.$channel.'" does not exist.';
throw new \InvalidArgumentException($msg, 0, $e);
}
$def->addMethodCall('pushHandler', [new Reference('ekino.new_relic.logs_handler')]);
}
}
private function getChannels(ContainerBuilder $container)
{
$channels = [];
foreach ($container->getDefinitions() as $id => $definition) {
if ('monolog.logger' === $id) {
$channels[] = 'app';
continue;
}
if (0 === strpos($id, 'monolog.logger.')) {
$channels[] = substr($id, 15);
}
}
return $channels;
}
}
================================================
FILE: DependencyInjection/Configuration.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\DependencyInjection;
use Psr\Log\LogLevel;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Twig\Environment;
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('ekino_new_relic');
if (method_exists(TreeBuilder::class, 'getRootNode')) {
$rootNode = $treeBuilder->getRootNode();
} else {
$rootNode = $treeBuilder->root('ekino_new_relic');
}
$rootNode
->fixXmlConfig('deployment_name')
->children()
->booleanNode('enabled')->defaultTrue()->end()
->scalarNode('interactor')->end()
->booleanNode('twig')->defaultValue(class_exists(Environment::class))->end()
->scalarNode('api_key')->defaultValue(null)->end()
->scalarNode('api_host')->defaultValue(null)->end()
->scalarNode('license_key')->defaultValue(null)->end()
->scalarNode('application_name')->defaultValue(null)->end()
->arrayNode('deployment_names')
->prototype('scalar')
->end()
->beforeNormalization()
->ifTrue(function ($v) { return !\is_array($v); })
->then(function ($v) { return array_values(array_filter(explode(';', (string) $v))); })
->end()
->end()
->scalarNode('xmit')->defaultValue(false)->end()
->booleanNode('logging')
->info('Write logs to a PSR3 logger whenever we send data to NewRelic.')
->defaultFalse()
->end()
->arrayNode('exceptions')
->canBeDisabled()
->end()
->arrayNode('commands')
->canBeDisabled()
->children()
->arrayNode('ignored_commands')
->prototype('scalar')
->end()
->beforeNormalization()
->ifTrue(function ($v) { return !\is_array($v); })
->then(function ($v) { return (array) $v; })
->end()
->end()
->end()
->end()
->arrayNode('deprecations')
->canBeDisabled()
->end()
->arrayNode('http')
->canBeDisabled()
->children()
->scalarNode('transaction_naming')
->defaultValue('route')
->validate()
->ifNotInArray(['route', 'controller', 'service'])
->thenInvalid('Invalid transaction naming scheme "%s", must be "route", "controller" or "service".')
->end()
->end()
->scalarNode('transaction_naming_service')->defaultNull()->end()
->arrayNode('ignored_routes')
->prototype('scalar')
->end()
->beforeNormalization()
->ifTrue(function ($v) { return !\is_array($v); })
->then(function ($v) { return (array) $v; })
->end()
->end()
->arrayNode('ignored_paths')
->prototype('scalar')
->end()
->beforeNormalization()
->ifTrue(function ($v) { return !\is_array($v); })
->then(function ($v) { return (array) $v; })
->end()
->end()
->scalarNode('using_symfony_cache')->defaultFalse()->end()
->end()
->end()
->booleanNode('instrument')
->defaultFalse()
->end()
->arrayNode('monolog')
->canBeEnabled()
->children()
->arrayNode('channels')
->fixXmlConfig('channel', 'elements')
->canBeUnset()
->beforeNormalization()
->ifString()
->then(function ($v) { return ['elements' => [$v]]; })
->end()
->beforeNormalization()
->ifTrue(function ($v) { return \is_array($v) && is_numeric(key($v)); })
->then(function ($v) { return ['elements' => $v]; })
->end()
->validate()
->ifTrue(function ($v) { return empty($v); })
->thenUnset()
->end()
->validate()
->always(function ($v) {
$isExclusive = null;
if (isset($v['type'])) {
$isExclusive = 'exclusive' === $v['type'];
}
$elements = [];
foreach ($v['elements'] as $element) {
if (0 === strpos($element, '!')) {
if (false === $isExclusive) {
throw new InvalidConfigurationException('Cannot combine exclusive/inclusive definitions in channels list.');
}
$elements[] = substr($element, 1);
$isExclusive = true;
} else {
if (true === $isExclusive) {
throw new InvalidConfigurationException('Cannot combine exclusive/inclusive definitions in channels list');
}
$elements[] = $element;
$isExclusive = false;
}
}
if (!\count($elements)) {
return;
}
return ['type' => $isExclusive ? 'exclusive' : 'inclusive', 'elements' => $elements];
})
->end()
->children()
->scalarNode('type')
->validate()
->ifNotInArray(['inclusive', 'exclusive'])
->thenInvalid('The type of channels has to be inclusive or exclusive')
->end()
->end()
->arrayNode('elements')
->prototype('scalar')->end()
->end()
->end()
->end()
->scalarNode('level')->defaultValue(LogLevel::ERROR)->end()
->scalarNode('service')->defaultValue('ekino.new_relic.monolog_handler')->end()
->end()
->end()
->end()
;
return $treeBuilder;
}
}
================================================
FILE: DependencyInjection/EkinoNewRelicExtension.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\DependencyInjection;
use Ekino\NewRelicBundle\Listener\CommandListener;
use Ekino\NewRelicBundle\Listener\RequestListener;
use Ekino\NewRelicBundle\Listener\ResponseListener;
use Ekino\NewRelicBundle\NewRelic\AdaptiveInteractor;
use Ekino\NewRelicBundle\NewRelic\BlackholeInteractor;
use Ekino\NewRelicBundle\NewRelic\Config;
use Ekino\NewRelicBundle\NewRelic\LoggingInteractorDecorator;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractor;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use Ekino\NewRelicBundle\TransactionNamingStrategy\ControllerNamingStrategy;
use Ekino\NewRelicBundle\TransactionNamingStrategy\RouteNamingStrategy;
use Ekino\NewRelicBundle\TransactionNamingStrategy\TransactionNamingStrategyInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
/**
* This is the class that loads and manages your bundle configuration.
*
* To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html}
*/
class EkinoNewRelicExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container): void
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.xml');
$container->setAlias(NewRelicInteractorInterface::class, $this->getInteractorServiceId($config))->setPublic(false);
$container->setAlias(TransactionNamingStrategyInterface::class, $this->getTransactionNamingServiceId($config))->setPublic(false);
if ($config['logging']) {
$container->register(LoggingInteractorDecorator::class)
->setDecoratedService(NewRelicInteractorInterface::class)
->setArguments(
[
'$interactor' => new Reference(LoggingInteractorDecorator::class.'.inner'),
'$logger' => new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE),
]
)
->setPublic(false)
;
}
if (empty($config['deployment_names'])) {
$config['deployment_names'] = array_values(array_filter(explode(';', $config['application_name'] ?? '')));
}
$container->getDefinition(Config::class)
->setArguments(
[
'$name' => $config['application_name'],
'$apiKey' => $config['api_key'],
'$licenseKey' => $config['license_key'],
'$xmit' => $config['xmit'],
'$deploymentNames' => $config['deployment_names'],
'$apiHost' => $config['api_host'],
]
);
if ($config['http']['enabled']) {
$loader->load('http_listener.xml');
$container->getDefinition(RequestListener::class)
->setArguments(
[
'$ignoreRoutes' => $config['http']['ignored_routes'],
'$ignoredPaths' => $config['http']['ignored_paths'],
'$symfonyCache' => $config['http']['using_symfony_cache'],
]
);
$container->getDefinition(ResponseListener::class)
->setArguments(
[
'$instrument' => $config['instrument'],
'$symfonyCache' => $config['http']['using_symfony_cache'],
]
);
}
if ($config['commands']['enabled']) {
$loader->load('command_listener.xml');
$container->getDefinition(CommandListener::class)
->setArguments(
[
'$ignoredCommands' => $config['commands']['ignored_commands'],
]
);
}
if ($config['exceptions']['enabled']) {
$loader->load('exception_listener.xml');
}
if ($config['deprecations']['enabled']) {
$loader->load('deprecation_listener.xml');
}
if ($config['twig']) {
$loader->load('twig.xml');
}
if ($config['enabled'] && $config['monolog']['enabled']) {
if (!class_exists(\Monolog\Handler\NewRelicHandler::class)) {
throw new \LogicException('The "symfony/monolog-bundle" package must be installed in order to use "monolog" option.');
}
$loader->load('monolog.xml');
$container->setParameter('ekino.new_relic.monolog', $config['monolog'] ?? []);
$container->setParameter('ekino.new_relic.application_name', $config['application_name']);
$container->setAlias('ekino.new_relic.logs_handler', $config['monolog']['service'])->setPublic(false);
}
}
private function getInteractorServiceId(array $config): string
{
if (!$config['enabled']) {
return BlackholeInteractor::class;
}
if (!isset($config['interactor'])) {
// Fallback on AdaptiveInteractor.
return AdaptiveInteractor::class;
}
if ('auto' === $config['interactor']) {
// Check if the extension is loaded or not
return \extension_loaded('newrelic') ? NewRelicInteractor::class : BlackholeInteractor::class;
}
return $config['interactor'];
}
private function getTransactionNamingServiceId(array $config): string
{
switch ($config['http']['transaction_naming']) {
case 'controller':
return ControllerNamingStrategy::class;
case 'route':
return RouteNamingStrategy::class;
case 'service':
if (!isset($config['http']['transaction_naming_service'])) {
throw new \LogicException('When using the "service", transaction naming scheme, the "transaction_naming_service" config parameter must be set.');
}
return $config['http']['transaction_naming_service'];
default:
throw new \InvalidArgumentException(sprintf('Invalid transaction naming scheme "%s", must be "route", "controller" or "service".', $config['http']['transaction_naming']));
}
}
}
================================================
FILE: EkinoNewRelicBundle.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle;
use Ekino\NewRelicBundle\DependencyInjection\Compiler\MonologHandlerPass;
use Ekino\NewRelicBundle\Listener\DeprecationListener;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class EkinoNewRelicBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new MonologHandlerPass());
}
public function boot()
{
parent::boot();
if ($this->container->has(DeprecationListener::class)) {
$this->container->get(DeprecationListener::class)->register();
}
}
public function shutdown()
{
if ($this->container->has(DeprecationListener::class)) {
$this->container->get(DeprecationListener::class)->unregister();
}
parent::shutdown();
}
}
================================================
FILE: Exception/DeprecationException.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Exception;
/**
* Exception dedicated to report Deprecation.
*/
class DeprecationException extends \ErrorException
{
}
================================================
FILE: LICENSE
================================================
The MIT License
Copyright (c) 2012 Ekino - thomas.rabaix@ekino.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: Listener/CommandListener.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Listener;
use Ekino\NewRelicBundle\NewRelic\Config;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class CommandListener implements EventSubscriberInterface
{
private $interactor;
private $config;
private $ignoredCommands;
public function __construct(Config $config, NewRelicInteractorInterface $interactor, array $ignoredCommands)
{
$this->config = $config;
$this->interactor = $interactor;
$this->ignoredCommands = $ignoredCommands;
}
public static function getSubscribedEvents(): array
{
return [
ConsoleEvents::COMMAND => ['onConsoleCommand', 0],
ConsoleEvents::ERROR => ['onConsoleError', 0],
];
}
public function onConsoleCommand(ConsoleCommandEvent $event): void
{
$command = $event->getCommand();
$input = $event->getInput();
if ($this->config->getName()) {
$this->interactor->setApplicationName($this->config->getName(), $this->config->getLicenseKey(), $this->config->getXmit());
}
$this->interactor->setTransactionName($command->getName());
// Due to newrelic's extension implementation, the method `ignoreTransaction` must be called after `setApplicationName`
// see https://discuss.newrelic.com/t/newrelic-ignore-transaction-not-being-honored/5450/5
if (\in_array($command->getName(), $this->ignoredCommands, true)) {
$this->interactor->ignoreTransaction();
}
$this->interactor->enableBackgroundJob();
// send parameters to New Relic
foreach ($input->getOptions() as $key => $value) {
$key = '--'.$key;
if (\is_array($value)) {
foreach ($value as $k => $v) {
$this->interactor->addCustomParameter($key.'['.$k.']', $v);
}
} else {
$this->interactor->addCustomParameter($key, $value);
}
}
foreach ($input->getArguments() as $key => $value) {
if (\is_array($value)) {
foreach ($value as $k => $v) {
$this->interactor->addCustomParameter($key.'['.$k.']', $v);
}
} else {
$this->interactor->addCustomParameter($key, $value);
}
}
}
public function onConsoleError(ConsoleErrorEvent $event): void
{
$this->interactor->noticeThrowable($event->getError());
}
}
================================================
FILE: Listener/DeprecationListener.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Listener;
use Ekino\NewRelicBundle\Exception\DeprecationException;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
class DeprecationListener
{
private $isRegistered = false;
private $interactor;
public function __construct(NewRelicInteractorInterface $interactor)
{
$this->interactor = $interactor;
}
public function register(): void
{
if ($this->isRegistered) {
return;
}
$this->isRegistered = true;
$prevErrorHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) use (&$prevErrorHandler) {
if (\E_USER_DEPRECATED === $type) {
$this->interactor->noticeThrowable(new DeprecationException($msg, 0, $type, $file, $line));
}
return $prevErrorHandler ? $prevErrorHandler($type, $msg, $file, $line, $context) : false;
});
}
public function unregister(): void
{
if (!$this->isRegistered) {
return;
}
$this->isRegistered = false;
restore_error_handler();
}
}
================================================
FILE: Listener/ExceptionListener.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Listener;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Listen to exceptions dispatched by Symfony to log them to NewRelic.
*/
class ExceptionListener implements EventSubscriberInterface
{
private $interactor;
public function __construct(NewRelicInteractorInterface $interactor)
{
$this->interactor = $interactor;
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::EXCEPTION => ['onKernelException', 0],
];
}
/**
* @param GetResponseForExceptionEvent|ExceptionEvent $event
*/
public function onKernelException(KernelExceptionEvent $event): void
{
$exception = method_exists($event, 'getThrowable') ? $event->getThrowable() : $event->getException();
if (!$exception instanceof HttpExceptionInterface) {
$this->interactor->noticeThrowable($exception);
}
}
}
if (!class_exists(KernelExceptionEvent::class)) {
if (class_exists(ExceptionEvent::class)) {
class_alias(ExceptionEvent::class, KernelExceptionEvent::class);
} else {
class_alias(GetResponseForExceptionEvent::class, KernelExceptionEvent::class);
}
}
================================================
FILE: Listener/RequestListener.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Listener;
use Ekino\NewRelicBundle\NewRelic\Config;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use Ekino\NewRelicBundle\TransactionNamingStrategy\TransactionNamingStrategyInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
class RequestListener implements EventSubscriberInterface
{
private $ignoredRoutes;
private $ignoredPaths;
private $config;
private $interactor;
private $transactionNamingStrategy;
private $symfonyCache;
public function __construct(
Config $config,
NewRelicInteractorInterface $interactor,
array $ignoreRoutes,
array $ignoredPaths,
TransactionNamingStrategyInterface $transactionNamingStrategy,
bool $symfonyCache = false
) {
$this->config = $config;
$this->interactor = $interactor;
$this->ignoredRoutes = $ignoreRoutes;
$this->ignoredPaths = $ignoredPaths;
$this->transactionNamingStrategy = $transactionNamingStrategy;
$this->symfonyCache = $symfonyCache;
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => [
['setApplicationName', 255],
['setIgnoreTransaction', 31],
['setTransactionName', -10],
],
];
}
public function setApplicationName(KernelRequestEvent $event): void
{
if (!$this->isEventValid($event)) {
return;
}
$appName = $this->config->getName();
if (!$appName) {
return;
}
if ($this->symfonyCache) {
$this->interactor->startTransaction($appName);
}
// Set application name if different from ini configuration
if ($appName !== ini_get('newrelic.appname')) {
$this->interactor->setApplicationName($appName, $this->config->getLicenseKey(), $this->config->getXmit());
}
}
public function setTransactionName(KernelRequestEvent $event): void
{
if (!$this->isEventValid($event)) {
return;
}
$transactionName = $this->transactionNamingStrategy->getTransactionName($event->getRequest());
$this->interactor->setTransactionName($transactionName);
}
public function setIgnoreTransaction(KernelRequestEvent $event): void
{
if (!$this->isEventValid($event)) {
return;
}
$request = $event->getRequest();
if (\in_array($request->get('_route'), $this->ignoredRoutes, true)) {
$this->interactor->ignoreTransaction();
}
if (\in_array($request->getPathInfo(), $this->ignoredPaths, true)) {
$this->interactor->ignoreTransaction();
}
}
/**
* Make sure we should consider this event. Example: make sure it is a master request.
*/
private function isEventValid(KernelRequestEvent $event): bool
{
return HttpKernelInterface::MASTER_REQUEST === $event->getRequestType();
}
}
if (!class_exists(KernelRequestEvent::class)) {
if (class_exists(RequestEvent::class)) {
class_alias(RequestEvent::class, KernelRequestEvent::class);
} else {
class_alias(GetResponseEvent::class, KernelRequestEvent::class);
}
}
================================================
FILE: Listener/ResponseListener.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Listener;
use Ekino\NewRelicBundle\NewRelic\Config;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use Ekino\NewRelicBundle\Twig\NewRelicExtension;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class ResponseListener implements EventSubscriberInterface
{
private $newRelic;
private $interactor;
private $instrument;
private $symfonyCache;
private $newRelicTwigExtension;
public function __construct(
Config $newRelic,
NewRelicInteractorInterface $interactor,
bool $instrument = false,
bool $symfonyCache = false,
NewRelicExtension $newRelicTwigExtension = null
) {
$this->newRelic = $newRelic;
$this->interactor = $interactor;
$this->instrument = $instrument;
$this->symfonyCache = $symfonyCache;
$this->newRelicTwigExtension = $newRelicTwigExtension;
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::RESPONSE => [
['onKernelResponse', -255],
],
];
}
public function onKernelResponse(KernelResponseEvent $event): void
{
$isMainRequest = method_exists($event, 'isMainRequest') ? $event->isMainRequest() : $event->isMasterRequest();
if (!$isMainRequest) {
return;
}
if (null === $this->newRelicTwigExtension || false === $this->newRelicTwigExtension->isUsed()) {
foreach ($this->newRelic->getCustomMetrics() as $name => $value) {
$this->interactor->addCustomMetric((string) $name, (float) $value);
}
foreach ($this->newRelic->getCustomParameters() as $name => $value) {
$this->interactor->addCustomParameter((string) $name, $value);
}
}
foreach ($this->newRelic->getCustomEvents() as $name => $events) {
foreach ($events as $attributes) {
$this->interactor->addCustomEvent((string) $name, $attributes);
}
}
if ($this->instrument) {
if (null === $this->newRelicTwigExtension || false === $this->newRelicTwigExtension->isUsed()) {
$this->interactor->disableAutoRUM();
}
// Some requests might not want to get instrumented
if ($event->getRequest()->attributes->get('_instrument', true)) {
$response = $event->getResponse();
// We can only instrument HTML responses
if (!$response instanceof StreamedResponse
&& 'text/html' === substr($response->headers->get('Content-Type', ''), 0, 9)
) {
$responseContent = $response->getContent();
$response->setContent(''); // free the memory
if (null === $this->newRelicTwigExtension || false === $this->newRelicTwigExtension->isHeaderCalled()) {
$responseContent = preg_replace('|<head>|i', '$0'.$this->interactor->getBrowserTimingHeader(), $responseContent);
}
if (null === $this->newRelicTwigExtension || false === $this->newRelicTwigExtension->isFooterCalled()) {
$responseContent = preg_replace('|</body>|i', $this->interactor->getBrowserTimingFooter().'$0', $responseContent);
}
$response->setContent($responseContent);
}
}
}
if ($this->symfonyCache) {
$this->interactor->endTransaction();
}
}
}
if (!class_exists(KernelResponseEvent::class)) {
if (class_exists(ResponseEvent::class)) {
class_alias(ResponseEvent::class, KernelResponseEvent::class);
} else {
class_alias(FilterResponseEvent::class, KernelResponseEvent::class);
}
}
================================================
FILE: Logging/AdaptiveHandler.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Logging;
use Monolog\Handler\NewRelicHandler;
use Psr\Log\LogLevel;
class AdaptiveHandler extends NewRelicHandler
{
public function __construct(
string $level = LogLevel::ERROR,
bool $bubble = true,
string $appName = null,
bool $explodeArrays = false,
string $transactionName = null
) {
parent::__construct($level, $bubble, $appName, $explodeArrays, $transactionName);
}
protected function write(array $record): void
{
if (!$this->isNewRelicEnabled()) {
return;
}
parent::write($record);
}
}
================================================
FILE: NewRelic/AdaptiveInteractor.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\NewRelic;
/**
* This interactor does never assume that the NewRelic extension is installed. It will check
* for the existence of the NewRelic extension every time this is class is instantiated. This
* is a good interactor to use when you want to enable and disable the NewRelic extension
* without rebuilding your container.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class AdaptiveInteractor implements NewRelicInteractorInterface
{
private $interactor;
public function __construct(NewRelicInteractorInterface $real, NewRelicInteractorInterface $fake)
{
$this->interactor = \extension_loaded('newrelic') ? $real : $fake;
}
public function setApplicationName(string $name, string $license = null, bool $xmit = false): bool
{
return $this->interactor->setApplicationName($name, $license, $xmit);
}
public function setTransactionName(string $name): bool
{
return $this->interactor->setTransactionName($name);
}
public function ignoreTransaction(): void
{
$this->interactor->ignoreTransaction();
}
public function addCustomEvent(string $name, array $attributes): void
{
$this->interactor->addCustomEvent($name, $attributes);
}
public function addCustomMetric(string $name, float $value): bool
{
return $this->interactor->addCustomMetric($name, $value);
}
public function addCustomParameter(string $name, $value): bool
{
return $this->interactor->addCustomParameter($name, $value);
}
public function getBrowserTimingHeader(bool $includeTags = true): string
{
return $this->interactor->getBrowserTimingHeader($includeTags);
}
public function getBrowserTimingFooter(bool $includeTags = true): string
{
return $this->interactor->getBrowserTimingFooter($includeTags);
}
public function disableAutoRUM(): ?bool
{
return $this->interactor->disableAutoRUM();
}
public function noticeThrowable(\Throwable $e, string $message = null): void
{
$this->interactor->noticeThrowable($e, $message);
}
public function noticeError(
int $errno,
string $errstr,
string $errfile = null,
int $errline = null,
string $errcontext = null
): void {
$this->interactor->noticeError($errno, $errstr, $errfile, $errline, $errcontext);
}
public function enableBackgroundJob(): void
{
$this->interactor->enableBackgroundJob();
}
public function disableBackgroundJob(): void
{
$this->interactor->disableBackgroundJob();
}
public function startTransaction(string $name = null, string $license = null): bool
{
return $this->interactor->startTransaction($name, $license);
}
public function endTransaction(bool $ignore = false): bool
{
return $this->interactor->endTransaction($ignore);
}
public function excludeFromApdex(): void
{
$this->interactor->excludeFromApdex();
}
public function addCustomTracer(string $name): bool
{
return $this->interactor->addCustomTracer($name);
}
public function setCaptureParams(bool $enabled): void
{
$this->interactor->setCaptureParams($enabled);
}
public function stopTransactionTiming(): void
{
$this->interactor->stopTransactionTiming();
}
public function recordDatastoreSegment(callable $func, array $parameters)
{
return $this->interactor->recordDatastoreSegment($func, $parameters);
}
public function setUserAttributes(string $userValue, string $accountValue, string $productValue): bool
{
return $this->interactor->setUserAttributes($userValue, $accountValue, $productValue);
}
public function getTraceMetadata(): array
{
if (!method_exists($this->interactor, 'getTraceMetadata')) {
throw new \BadMethodCallException('The decorated interaction does not implement this method');
}
return $this->interactor->getTraceMetadata();
}
public function getLinkingMetadata(): array
{
if (!method_exists($this->interactor, 'getLinkingMetadata')) {
throw new \BadMethodCallException('The decorated interaction does not implement this method');
}
return $this->interactor->getLinkingMetadata();
}
public function isSampled(): bool
{
if (!method_exists($this->interactor, 'isSampled')) {
throw new \BadMethodCallException('The decorated interaction does not implement this method');
}
return $this->interactor->isSampled();
}
public function insertDistributedTracingHeaders(array &$headers): void
{
if (!method_exists($this->interactor, 'insertDistributedTracingHeaders')) {
throw new \BadMethodCallException('The decorated interaction does not implement this method');
}
$this->interactor->insertDistributedTracingHeaders($headers);
}
public function acceptDistributedTraceHeaders(array $headers, string $transportType = 'HTTP'): void
{
if (!method_exists($this->interactor, 'acceptDistributedTraceHeaders')) {
throw new \BadMethodCallException('The decorated interaction does not implement this method');
}
$this->interactor->acceptDistributedTraceHeaders($headers, $transportType);
}
}
================================================
FILE: NewRelic/BlackholeInteractor.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\NewRelic;
/**
* This interactor throw away any call.
*
* It can be used to avoid conditional log calls.
*/
class BlackholeInteractor implements NewRelicInteractorInterface
{
public function setApplicationName(string $name, string $license = null, bool $xmit = false): bool
{
return true;
}
public function setTransactionName(string $name): bool
{
return true;
}
public function ignoreTransaction(): void
{
}
public function addCustomEvent(string $name, array $attributes): void
{
}
public function addCustomMetric(string $name, float $value): bool
{
return true;
}
public function addCustomParameter(string $name, $value): bool
{
return true;
}
public function getBrowserTimingHeader(bool $includeTags = true): string
{
return '';
}
public function getBrowserTimingFooter(bool $includeTags = true): string
{
return '';
}
public function disableAutoRUM(): ?bool
{
return true;
}
public function noticeThrowable(\Throwable $e, string $message = null): void
{
}
public function noticeError(
int $errno,
string $errstr,
string $errfile = null,
int $errline = null,
string $errcontext = null
): void {
}
public function enableBackgroundJob(): void
{
}
public function disableBackgroundJob(): void
{
}
public function startTransaction(string $name = null, string $license = null): bool
{
return true;
}
public function endTransaction(bool $ignore = false): bool
{
return true;
}
public function excludeFromApdex(): void
{
}
public function addCustomTracer(string $name): bool
{
return true;
}
public function setCaptureParams(bool $enabled): void
{
}
public function stopTransactionTiming(): void
{
}
public function recordDatastoreSegment(callable $func, array $parameters)
{
return $func();
}
public function setUserAttributes(string $userValue, string $accountValue, string $productValue): bool
{
return true;
}
public function getTraceMetadata(): array
{
return [];
}
public function getLinkingMetadata(): array
{
return [];
}
public function isSampled(): bool
{
return true;
}
public function insertDistributedTracingHeaders(array &$headers): void
{
}
public function acceptDistributedTraceHeaders(array $headers, string $transportType = 'HTTP'): void
{
}
}
================================================
FILE: NewRelic/Config.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\NewRelic;
/**
* This value object contains data and configuration that should be passed to the interactors.
*/
class Config
{
private $name;
private $apiKey;
private $apiHost = null;
private $licenseKey;
private $xmit;
private $customEvents;
private $customMetrics;
private $customParameters;
private $deploymentNames;
public function __construct(?string $name, string $apiKey = null, string $licenseKey = null, bool $xmit = false, array $deploymentNames = [], ?string $apiHost = null)
{
$this->name = (!empty($name) ? $name : ini_get('newrelic.appname')) ?: '';
$this->apiKey = $apiKey;
$this->apiHost = $apiHost;
$this->licenseKey = (!empty($licenseKey) ? $licenseKey : ini_get('newrelic.license')) ?: '';
$this->xmit = $xmit;
$this->deploymentNames = $deploymentNames;
$this->customEvents = [];
$this->customMetrics = [];
$this->customParameters = [];
}
public function setCustomEvents(array $customEvents): void
{
$this->customEvents = $customEvents;
}
public function getCustomEvents(): array
{
return $this->customEvents;
}
public function addCustomEvent(string $name, array $attributes): void
{
$this->customEvents[$name][] = $attributes;
}
public function setCustomMetrics(array $customMetrics): void
{
$this->customMetrics = $customMetrics;
}
public function getCustomMetrics(): array
{
return $this->customMetrics;
}
public function setCustomParameters(array $customParameters): void
{
$this->customParameters = $customParameters;
}
/**
* @param string|int|float $value or any scalar value
*/
public function addCustomParameter(string $name, $value): void
{
$this->customParameters[$name] = $value;
}
public function addCustomMetric(string $name, float $value): void
{
$this->customMetrics[$name] = $value;
}
public function getCustomParameters(): array
{
return $this->customParameters;
}
public function getName(): string
{
return $this->name;
}
/**
* @return string[]
*/
public function getDeploymentNames(): array
{
return $this->deploymentNames;
}
public function getApiKey(): ?string
{
return $this->apiKey;
}
public function getApiHost(): ?string
{
return $this->apiHost;
}
public function getLicenseKey(): ?string
{
return $this->licenseKey;
}
public function getXmit(): bool
{
return $this->xmit;
}
}
================================================
FILE: NewRelic/LoggingInteractorDecorator.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\NewRelic;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
class LoggingInteractorDecorator implements NewRelicInteractorInterface
{
private $interactor;
private $logger;
public function __construct(NewRelicInteractorInterface $interactor, LoggerInterface $logger = null)
{
$this->interactor = $interactor;
$this->logger = $logger ?? new NullLogger();
}
public function setApplicationName(string $name, string $license = null, bool $xmit = false): bool
{
$this->logger->debug('Setting New Relic Application name to {name}', ['name' => $name]);
return $this->interactor->setApplicationName($name, $license, $xmit);
}
public function setTransactionName(string $name): bool
{
$this->logger->debug('Setting New Relic Transaction name to {name}', ['name' => $name]);
return $this->interactor->setTransactionName($name);
}
public function ignoreTransaction(): void
{
$this->logger->debug('Ignoring transaction');
$this->interactor->ignoreTransaction();
}
public function addCustomEvent(string $name, array $attributes): void
{
$this->logger->debug('Adding custom New Relic event {name}', ['name' => $name, 'attributes' => $attributes]);
$this->interactor->addCustomEvent($name, $attributes);
}
public function addCustomMetric(string $name, float $value): bool
{
$this->logger->debug('Adding custom New Relic metric {name}: {value}', ['name' => $name, 'value' => $value]);
return $this->interactor->addCustomMetric($name, $value);
}
public function addCustomParameter(string $name, $value): bool
{
$this->logger->debug('Adding custom New Relic parameters {name}: {value}', ['name' => $name, 'value' => $value]);
return $this->interactor->addCustomParameter($name, $value);
}
public function getBrowserTimingHeader(bool $includeTags = true): string
{
$this->logger->debug('Getting New Relic RUM timing header');
return $this->interactor->getBrowserTimingHeader($includeTags);
}
public function getBrowserTimingFooter(bool $includeTags = true): string
{
$this->logger->debug('Getting New Relic RUM timing footer');
return $this->interactor->getBrowserTimingFooter($includeTags);
}
public function disableAutoRUM(): ?bool
{
$this->logger->debug('Disabling New Relic Auto-RUM');
return $this->interactor->disableAutoRUM();
}
public function noticeError(
int $errno,
string $errstr,
string $errfile = null,
int $errline = null,
string $errcontext = null
): void {
$this->logger->debug('Sending notice error to New Relic', [
'error_code' => $errno,
'message' => $errstr,
'file' => $errfile,
'line' => $errline,
'context_error' => $errcontext,
]);
$this->interactor->noticeError($errno, $errstr, $errfile, $errline, $errcontext);
}
public function noticeThrowable(\Throwable $e, string $message = null): void
{
$this->logger->debug('Sending exception to New Relic', [
'message' => $message,
'exception' => $e,
]);
$this->interactor->noticeThrowable($e, $message);
}
public function enableBackgroundJob(): void
{
$this->logger->debug('Enabling New Relic background job');
$this->interactor->enableBackgroundJob();
}
public function disableBackgroundJob(): void
{
$this->logger->debug('Disabling New Relic background job');
$this->interactor->disableBackgroundJob();
}
public function endTransaction(bool $ignore = false): bool
{
$this->logger->debug('Ending a New Relic transaction');
return $this->interactor->endTransaction($ignore);
}
public function startTransaction(string $name = null, string $license = null): bool
{
$this->logger->debug('Starting a new New Relic transaction for app {name}', ['name' => $name]);
return $this->interactor->startTransaction($name, $license);
}
public function excludeFromApdex(): void
{
$this->logger->debug('Excluding current transaction from New Relic Apdex score');
$this->interactor->excludeFromApdex();
}
public function addCustomTracer(string $name): bool
{
$this->logger->debug('Adding custom New Relic tracer', ['name' => $name]);
return $this->interactor->addCustomTracer($name);
}
public function setCaptureParams(bool $enabled): void
{
$this->logger->debug('Toggle New Relic capture params to {enabled}', ['enabled' => $enabled]);
$this->interactor->setCaptureParams($enabled);
}
public function stopTransactionTiming(): void
{
$this->logger->debug('Stopping New Relic timing');
$this->interactor->stopTransactionTiming();
}
public function recordDatastoreSegment(callable $func, array $parameters)
{
$this->logger->debug('Adding custom New Relic datastore segment', [
'parameters' => $parameters,
]);
return $this->interactor->recordDatastoreSegment($func, $parameters);
}
public function setUserAttributes(string $userValue, string $accountValue, string $productValue): bool
{
$this->logger->debug('Setting New Relic user attributes', [
'user_value' => $userValue,
'account_value' => $accountValue,
'product_value' => $productValue,
]);
return $this->interactor->setUserAttributes($userValue, $accountValue, $productValue);
}
public function getTraceMetadata(): array
{
$traceMetadata = $this->interactor->getTraceMetadata();
$this->logger->debug('Getting New Relic trace metadata', $traceMetadata);
return $traceMetadata;
}
public function getLinkingMetadata(): array
{
$linkingMetadata = $this->interactor->getLinkingMetadata();
$this->logger->debug('Getting New Relic linking metadata', $linkingMetadata);
return $linkingMetadata;
}
public function isSampled(): bool
{
$isSampled = $this->interactor->isSampled();
$this->logger->debug('Getting New Relic sampled status', ['sampled' => $isSampled]);
return $isSampled;
}
public function insertDistributedTracingHeaders(array &$headers): void
{
$this->logger->debug('Setting New Relic distributed tracing headers', ['headers' => $headers]);
$this->interactor->insertDistributedTracingHeaders($headers);
}
public function acceptDistributedTraceHeaders(array $headers, string $transportType = 'HTTP'): void
{
$this->logger->debug('Accepting New Relic distributed tracing headers', [
'headers' => $headers,
'transport_type' => $transportType,
]);
$this->interactor->acceptDistributedTraceHeaders($headers, $transportType);
}
}
================================================
FILE: NewRelic/NewRelicInteractor.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\NewRelic;
class NewRelicInteractor implements NewRelicInteractorInterface
{
public function setApplicationName(string $name, string $license = null, bool $xmit = false): bool
{
return newrelic_set_appname($name, $license, $xmit);
}
public function setTransactionName(string $name): bool
{
return newrelic_name_transaction($name);
}
public function ignoreTransaction(): void
{
newrelic_ignore_transaction();
}
public function addCustomEvent(string $name, array $attributes): void
{
newrelic_record_custom_event((string) $name, $attributes);
}
public function addCustomMetric(string $name, float $value): bool
{
return newrelic_custom_metric($name, $value);
}
public function addCustomParameter(string $name, $value): bool
{
return newrelic_add_custom_parameter((string) $name, $value);
}
public function getBrowserTimingHeader(bool $includeTags = true): string
{
return newrelic_get_browser_timing_header($includeTags);
}
public function getBrowserTimingFooter(bool $includeTags = true): string
{
return newrelic_get_browser_timing_footer($includeTags);
}
public function disableAutoRUM(): ?bool
{
return newrelic_disable_autorum();
}
public function noticeError(int $errno, string $errstr, string $errfile = null, int $errline = null, string $errcontext = null): void
{
newrelic_notice_error($errno, $errstr, $errfile, $errline, $errcontext);
}
public function noticeThrowable(\Throwable $e, string $message = null): void
{
newrelic_notice_error($message ?: $e->getMessage(), $e);
}
public function enableBackgroundJob(): void
{
newrelic_background_job(true);
}
public function disableBackgroundJob(): void
{
newrelic_background_job(false);
}
public function endTransaction(bool $ignore = false): bool
{
return newrelic_end_transaction($ignore);
}
public function startTransaction(string $name = null, string $license = null): bool
{
if (null === $name) {
$name = ini_get('newrelic.appname');
}
if (null === $license) {
return newrelic_start_transaction($name);
}
return newrelic_start_transaction($name, $license);
}
public function excludeFromApdex(): void
{
newrelic_ignore_apdex();
}
public function addCustomTracer(string $name): bool
{
return newrelic_add_custom_tracer($name);
}
public function setCaptureParams(bool $enabled): void
{
newrelic_capture_params($enabled);
}
public function stopTransactionTiming(): void
{
newrelic_end_of_transaction();
}
public function recordDatastoreSegment(callable $func, array $parameters)
{
return newrelic_record_datastore_segment($func, $parameters);
}
public function setUserAttributes(string $userValue, string $accountValue, string $productValue): bool
{
return newrelic_set_user_attributes($userValue, $accountValue, $productValue);
}
public function getTraceMetadata(): array
{
if (!function_exists('newrelic_get_trace_metadata')) {
throw new \BadMethodCallException('You need the "newrelic" extension version 9.3 or higher to use this method');
}
return newrelic_get_trace_metadata();
}
public function getLinkingMetadata(): array
{
if (!function_exists('newrelic_get_linking_metadata')) {
throw new \BadMethodCallException('You need the "newrelic" extension version 9.3 or higher to use this method');
}
return newrelic_get_linking_metadata();
}
public function isSampled(): bool
{
if (!function_exists('newrelic_is_sampled')) {
throw new \BadMethodCallException('You need the "newrelic" extension version 9.3 or higher to use this method');
}
return newrelic_is_sampled();
}
public function insertDistributedTracingHeaders(array &$headers): void
{
if (!function_exists('newrelic_insert_distributed_trace_headers')) {
throw new \BadMethodCallException('You need the "newrelic" extension version 9.8 or higher to use this method');
}
newrelic_insert_distributed_trace_headers($headers);
}
public function acceptDistributedTraceHeaders(array $headers, string $transportType = 'HTTP'): void
{
if (!function_exists('newrelic_accept_distributed_trace_headers')) {
throw new \BadMethodCallException('You need the "newrelic" extension version 9.8 or higher to use this method');
}
newrelic_accept_distributed_trace_headers($headers, $transportType);
}
}
================================================
FILE: NewRelic/NewRelicInteractorInterface.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\NewRelic;
/**
* This is the service that talks to NewRelic.
*
* @method array getTraceMetadata()
* @method array getLinkingMetadata()
* @method bool isSampled()
* @method void insertDistributedTracingHeaders(array &$headers)
* @method void acceptDistributedTraceHeaders(array $headers, string $transportType = 'HTTP')
*/
interface NewRelicInteractorInterface
{
/**
* Sets the New Relic app name, which controls data rollup.
*
* {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_set_appname}
*/
public function setApplicationName(string $name, string $license = null, bool $xmit = false): bool;
/**
* Set custom name for current transaction.
*
* {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_name_transaction}
*/
public function setTransactionName(string $name): bool;
/**
* Do not instrument the current transaction.
*
* {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_ignore_transaction}
*/
public function ignoreTransaction(): void;
/**
* Record a custom event with the given name and attributes.
*
* {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_record_custom_event}
*/
public function addCustomEvent(string $name, array $attributes): void;
/**
* Add a custom metric (in milliseconds) to time a component of your app not captured by default.
*
* {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newreliccustommetric-php-agent-api}
*/
public function addCustomMetric(string $name, float $value): bool;
/**
* {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_add_custom_parameter}.
*
* @param string|int|float $value should be a scalar
*/
public function addCustomParameter(string $name, $value): bool;
/**
* Returns a New Relic Browser snippet to inject in the head of your HTML output.
*
* {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_get_browser_timing_header}
*/
public function getBrowserTimingHeader(bool $includeTags = true): string;
/**
* Returns a New Relic Browser snippet to inject at the end of the HTML output.
*
* {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_get_browser_timing_footer}
*/
public function getBrowserTimingFooter(bool $includeTags = true): string;
/**
* Disable automatic injection of the New Relic Browser snippet on particular pages.
*
* {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_disable_autorum}
*/
public function disableAutoRUM(): ?bool;
/**
* Use these calls to collect errors that the PHP agent does not collect automatically and to set the callback for
* your own error and exception handler.
*
* {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_notice_error}
*/
public function noticeThrowable(\Throwable $e, string $message = null): void;
/**
* {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_notice_error}.
*/
public function noticeError(int $errno, string $errstr, string $errfile = null, int $errline = null, string $errcontext = null): void;
/**
* {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_background_job}.
*/
public function enableBackgroundJob(): void;
/**
* {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_background_job}.
*/
public function disableBackgroundJob(): void;
/**
* If you previously ended a transaction you many want to start a new one.
*
* {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_start_transaction}
*/
public function startTransaction(string $name = null, string $license = null): bool;
/**
* Stop instrumenting the current transaction immediately.
*
* {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_end_transaction}
*/
public function endTransaction(bool $ignore = false): bool;
/**
* Ignore the current transaction when calculating Apdex.
*
* {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_ignore_apdex}
*/
public function excludeFromApdex(): void;
/**
* Specify functions or methods for the agent to target for custom instrumentation. This is the API equivalent of
* the newrelic.transaction_tracer.custom setting.
*
* {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_add_custom_tracer}
*/
public function addCustomTracer(string $name): bool;
/**
* Enable or disable the capture of URL parameters.
*
* {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_capture_params}
*/
public function setCaptureParams(bool $enabled): void;
/**
* Stop timing the current transaction, but continue instrumenting it.
*
* {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_end_of_transaction}
*/
public function stopTransactionTiming(): void;
/**
* Records a datastore segment.
*
* {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_record_datastore_segment}
*
* @return bool|mixed The return value of $func is returned. If an error occurs, false is returned.
*/
public function recordDatastoreSegment(callable $func, array $parameters);
/**
* Create user-related custom attributes. newrelic_add_custom_parameter is more flexible.
*
* {@link https://docs.newrelic.com/docs/agents/php-agent/php-agent-api/newrelic_set_user_attributes}
*/
public function setUserAttributes(string $userValue, string $accountValue, string $productValue): bool;
}
================================================
FILE: README.md
================================================
Ekino NewRelic Bundle
=====================
[](http://travis-ci.org/ekino/EkinoNewRelicBundle)
[](https://github.com/ekino/EkinoNewRelicBundle/releases)
[](https://scrutinizer-ci.com/g/ekino/EkinoNewRelicBundle)
[](https://scrutinizer-ci.com/g/ekino/EkinoNewRelicBundle)
[](https://packagist.org/packages/ekino/newrelic-bundle)
This bundle integrates the NewRelic PHP API into Symfony. For more information about NewRelic, please visit http://newrelic.com. The built-in New Relic agent doesn't add as much Symfony integration as it claims. This bundle adds a lot more essentials. Here's a quick list:
1. **Better transaction naming strategy**: Your transaction traces can be named accurately by route names, the controller name or you can decide on a custom naming strategy via a seamless interface that uses any naming convention you deem fit. While running console commands, it also sets the transaction name as the command name.
2. **Console Commands Enhancements**: While running console commands, its sets the options and arguments passed via the CLI as custom parameters to the transaction trace for easier debugging.
3. **Exception Listening**: It also captures all Symfony exceptions in web requests and console commands and sends them to New Relic (something new relic doesn't do too well itself as symfony aggressively catches all exceptions/errors). It also ensures all HTTP Exceptions (4xx codes) are logged as notices in New Relic and not exceptions to reduce the noise in New Relic.
4. **Interactor Service**: It provides you the New Relic PHP Agent API via a Service class `NewRelicInteractorInterface::class` so in my code, I can inject it into any class, controller, service and do stuff like -
```php
// Bundle
$this->newRelic->addCustomParameter('name', 'john');
// Extension
if (extension_loaded('newrelic')) {
\newrelic_add_custom_parameter('name', 'john');
}
```
5. **Logging Support**: In development, you are unlikely to have New Relic setup. There's a configuration to enable logging which outputs all New Relic actions to your Symfony log, hence emulating what it would actually do in production.
6. **Ignored Routes, Paths, Commands**: You can configure a list of route name, url paths and console commands to be ignored from New Relic traces.

7. **Misc**: There are other useful configuration like your New Relic API Key, explicitly defining your app name instead of php.ini, notifying New Relic about new deployments via capifony, etc.

## Installation
### Step 0 : Install NewRelic
review http://newrelic.com ...
### Step 1: add dependency
```bash
$ composer require ekino/newrelic-bundle
```
### Step 2 : Register the bundle
Then register the bundle with your kernel:
```php
<?php
// in AppKernel::registerBundles()
$bundles = array(
// ...
new Ekino\NewRelicBundle\EkinoNewRelicBundle(),
// ...
);
```
### Step 3 : Configure the bundle
In New Relic's web interface, make sure to get a valid (REST) API Key, not to be confused with your License key : New Relic Dashboard > Account settings > Integration > API Keys
```yaml
# app/config/config.yml
ekino_new_relic:
enabled: true # Defaults to true
application_name: Awesome Application # default value in newrelic is "PHP Application", or whatever is set
# as php ini-value
deployment_names: ~ # default value is 'application_name', supports string array or semi-colon separated string
api_key: # New Relic API
api_host: ~ # New Relic API Host (default value is api.newrelic.com, for EU should be set to api.eu.newrelic.com )
license_key: # New Relic license key (optional, default value is read from php.ini)
xmit: false # if you want to record the metric data up to the point newrelic_set_appname is called, set this to true (default: false)
logging: false # If true, logs all New Relic interactions to the Symfony log (default: false)
interactor: ~ # The interactor service that is used. Setting enabled=false will override this value
twig: true # Allows you to disable twig integration (falls back to class_exists(\Twig_Environment::class))
exceptions: true # If true, sends exceptions to New Relic (default: true)
deprecations: true # If true, reports deprecations to New Relic (default: true)
instrument: false # If true, uses enhanced New Relic RUM instrumentation (see below) (default: false)
http:
enabled: true
using_symfony_cache: false # Symfony HTTP cache (see below) (default: false)
transaction_naming: route # route, controller or service (see below)
transaction_naming_service: ~ # Transaction naming service (see below)
ignored_routes: [] # No transaction recorded for this routes
ignored_paths: [] # No transaction recorded for this paths
monolog:
enabled: false # When enabled, send application's logs to New Relic (default: disabled)
channels: [app] # Channels to listen (default: null). [See Symfony's documentation](http://symfony.com/doc/current/logging/channels_handlers.html#yaml-specification)
level: error # Report only logs higher than this level (see \Psr\Log\LogLevel) (default: error)
service: app.my_custom_handler # Define a custom log handler (default: ekino.new_relic.monolog_handler)
commands:
enabled: true # If true, logs CLI commands to New Relic as Background jobs (>2.3 only) (default: true)
ignored_commands: [] # No transaction recorded for this commands (background tasks)
```
## Enhanced RUM instrumentation
The bundle comes with an option for enhanced real user monitoring. Ordinarily the New Relic extension (unless disabled by configuration) automatically adds a tracking code for RUM instrumentation to all HTML responses. Using enhanced RUM instrumentation, the bundle allows you to selectively disable instrumentation on certain requests.
This can be useful if, e.g. you're returning HTML verbatim for an HTML editor.
If enhanced RUM instrumentation is enabled, you can *disable* instrumentation for a given request by passing along a `_instrument` request parameter, and setting it to `false`. This can be done e.g. through the routing configuration.
## Transaction naming strategies
The bundle comes with two built-in transaction naming strategies. `route` and `controller`, naming the New Relic transaction after the route or controller respectively. However, the bundle supports custom transaction naming strategies through the `service` configuration option. If you have selected the `service` configuration option, you must pass the name of your own transaction naming service as the `transaction_naming_service` configuration option.
The transaction naming service class must implement the `Ekino\NewRelicBundle\TransactionNamingStrategy\TransactionNamingStrategyInterface` interface. For more information on creating your own services, see the Symfony documentation on [Creating/Configuring Services in the Container](http://symfony.com/doc/current/book/service_container.html#creating-configuring-services-in-the-container).
## Symfony HTTP Cache
When you are using Symfony's HTTP cache your `app/AppCache.php` will build up a response with your Edge Side Includes (ESI). This will look like one transaction in New Relic. When you set `using_symfony_cache: true` will these ESI request be separate transaction which improves the statistics. If you are using some other reverse proxy cache or no cache at all, leave this to false.
If true is required to set the `application_name`.
## Deployment notification
You can use the `newrelic:notify-deployment` command to send deployment notifications to New Relic. This requires the `api_key` configuration to be set.
The command has a bunch of options, as displayed in the help data.
```
$ app/console newrelic:notify-deployment --help
Usage:
newrelic:notify-deployment [--user[="..."]] [--revision[="..."]] [--changelog[="..."]] [--description[="..."]]
Options:
--user The name of the user/process that triggered this deployment
--revision A revision number (e.g., git commit SHA)
--changelog A list of changes for this deployment
--description Text annotation for the deployment — notes for you
```
The bundle provide a [Capifony](http://capifony.org) recipe to automate the deployment notifications (see `Resources/recipes/newrelic.rb`).
It makes one request per `app_name`, due roll-up names are not supported by Data REST API.
## Interactor services
The config key`ekino_new_relic.interactor` will accept a service ID to a service implementing `NewRelicInteractorInterface`.
This bundle comes with a few services that may be suitable for you.
| Configuration value | Description |
| ------------------- | ----------- |
| `Ekino\NewRelicBundle\NewRelic\AdaptiveInteractor` | This is the default interactor. It will check once per request if the NewRelic PHP extension is installed or not. It is a decorator for the `NewRelicInteractor` |
| `Ekino\NewRelicBundle\NewRelic\NewRelicInteractor` | This interactor communicates with NewRelic. It is the one decorator that actually does some work. |
| `Ekino\NewRelicBundle\NewRelic\BlackholeInteractor` | This interactor does nothing. |
| `auto` | This value will check if the NewRelic PHP extension is installed when you build your container. |
Note that if you set `ekino_new_relic.enabled: false` you will always use the `BlackholeInteractor` no matter what value
used for `ekino_new_relic.interactor`.
## Flow of the Request
1. A request comes in and the first thing we do is to `setApplicationName` so that we use the correct license key and name.
2. The `RouterListener` might throw a 404 or add routing values to the request.
3. If no 404 was thrown we `setIgnoreTransaction` which means that we call `NewRelicInteractorInterface::ignoreTransaction()` if we have configured to ignore the route.
4. The Firewall is the next interesting thing that will happen. It could change the controller or throw a 403.
5. The developer might have configured many more request listeners that will now execute and possibly add stuff to the request.
6. We will execute `setTransactionName` to use our `TransactionNamingStrategyInterface` to set a nice name.
All 6 steps will be executed for a normal request. Exceptions to this is 404 and 403 responses that will be created in
step 2 and step 4 respectively. If an exception to these step occurs (I'm not talking about `\Exception`) you will have
the transaction logged with the correct license key but you do not have the proper transaction name. The `setTransactionName` may
have dependencies on data set by other listeners that is why it has such low priority.
================================================
FILE: Resources/config/command_listener.xml
================================================
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<defaults autowire="true" autoconfigure="true" public="false" />
<service id="Ekino\NewRelicBundle\Listener\CommandListener"/>
</services>
</container>
================================================
FILE: Resources/config/deprecation_listener.xml
================================================
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<defaults autowire="true" autoconfigure="true" public="false" />
<service id="Ekino\NewRelicBundle\Listener\DeprecationListener" public="true" />
</services>
</container>
================================================
FILE: Resources/config/exception_listener.xml
================================================
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<defaults autowire="true" autoconfigure="true" public="false" />
<service id="Ekino\NewRelicBundle\Listener\ExceptionListener"/>
</services>
</container>
================================================
FILE: Resources/config/http_listener.xml
================================================
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<defaults autowire="true" autoconfigure="true" public="false" />
<service id="Ekino\NewRelicBundle\Listener\RequestListener"/>
<service id="Ekino\NewRelicBundle\Listener\ResponseListener"/>
</services>
</container>
================================================
FILE: Resources/config/monolog.xml
================================================
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<defaults autowire="true" autoconfigure="true" public="false" />
<prototype namespace="Ekino\NewRelicBundle\Logging\" resource="../../Logging/*" />
<!-- id must be defined to not override custom configuration of `Monolog\Handler\NewRelicHandler` -->
<service id="ekino.new_relic.monolog_handler" class="Monolog\Handler\NewRelicHandler" />
</services>
</container>
================================================
FILE: Resources/config/services.xml
================================================
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<defaults autowire="true" autoconfigure="true" public="false" />
<prototype namespace="Ekino\NewRelicBundle\Command\" resource="../../Command/*" />
<prototype namespace="Ekino\NewRelicBundle\NewRelic\" resource="../../NewRelic/*" />
<prototype namespace="Ekino\NewRelicBundle\TransactionNamingStrategy\" resource="../../TransactionNamingStrategy/*" />
<service id="Ekino\NewRelicBundle\NewRelic\AdaptiveInteractor">
<argument type="service" id="Ekino\NewRelicBundle\NewRelic\NewRelicInteractor" />
<argument type="service" id="Ekino\NewRelicBundle\NewRelic\BlackholeInteractor" />
</service>
</services>
</container>
================================================
FILE: Resources/config/twig.xml
================================================
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<defaults autowire="true" autoconfigure="true" public="false" />
<prototype namespace="Ekino\NewRelicBundle\Twig\" resource="../../Twig/*" />
</services>
</container>
================================================
FILE: Resources/recipes/newrelic.rb
================================================
namespace :newrelic do
# on all deployments, notify New Relic
desc "Record a deployment in New Relic (newrelic.com)"
task :notice_deployment, :roles => :app, :only => { :primary => true }, :except => { :no_release => true } do
begin
# allow overrides to be defined for revision, description, changelog
rev = fetch(:newrelic_revision) if exists?(:newrelic_revision)
description = fetch(:newrelic_desc) if exists?(:newrelic_desc)
changelog = fetch(:newrelic_changelog) if exists?(:newrelic_changelog)
user = fetch(:newrelic_user) if exists?(:newrelic_user)
if !changelog
logger.debug "Getting log of changes for New Relic Deployment details"
from_revision = source.local.next_revision(current_revision)
if scm == :git
log_command = "git log --no-color --pretty=format:' * [%ai] %an: %s' --abbrev-commit --no-merges #{previous_revision}..#{real_revision}"
else
log_command = "#{source.local.log(from_revision)}"
end
changelog = `#{log_command}`
end
if rev.nil?
rev = source.local.query_revision(source.local.head()) do |cmd|
logger.debug "executing locally: '#{cmd}'"
`#{cmd}`
end
rev = rev[0..6] if scm == :git
end
new_revision = rev
logger.debug "Uploading deployment to New Relic"
capifony_pretty_print "--> Notifying New Relic of deployment"
run "cd #{latest_release} && #{php_bin} #{symfony_console} newrelic:notify-deployment #{console_options} --revision=#{rev.shellescape} --changelog=#{changelog.to_s.shellescape} --description=#{description.to_s.shellescape} --user=#{user.to_s.shellescape}"
capifony_puts_ok
rescue Capistrano::CommandError
logger.info "Unable to notify New Relic of the deployment... skipping"
end
end
end
================================================
FILE: Tests/AppKernel.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Tests;
use Ekino\NewRelicBundle\EkinoNewRelicBundle;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Routing\RouteCollection;
class AppKernel extends Kernel
{
/**
* @var string
*/
private $cachePrefix = '';
/**
* @var string|null;
*/
private $fakedProjectDir;
/**
* @param string $cachePrefix
*/
public function __construct($cachePrefix)
{
parent::__construct($cachePrefix, true);
$this->cachePrefix = $cachePrefix;
}
public function getCacheDir(): string
{
return sys_get_temp_dir().'/ekino/'.$this->cachePrefix;
}
public function getLogDir(): string
{
return sys_get_temp_dir().'/ekino/log';
}
public function getProjectDir(): string
{
if (null === $this->fakedProjectDir) {
return realpath(__DIR__.'/../../../../');
}
return $this->fakedProjectDir;
}
/**
* @param string|null $rootDir
*/
public function setRootDir($rootDir)
{
$this->rootDir = $rootDir;
}
/**
* @param string|null $projectDir
*/
public function setProjectDir($projectDir)
{
$this->fakedProjectDir = $projectDir;
}
public function registerBundles(): iterable
{
return [
new FrameworkBundle(),
new EkinoNewRelicBundle(),
];
}
/**
* (From MicroKernelTrait)
* {@inheritdoc}
*/
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(function (ContainerBuilder $container) {
$container->loadFromExtension('framework', [
'secret' => 'test',
'router' => [
'resource' => 'kernel:loadRoutes',
'type' => 'service',
],
]);
// Not setting the router to utf8 is deprecated in symfony 5.1
if (Kernel::VERSION_ID >= 50100) {
$container->loadFromExtension('framework', [
'router' => ['utf8' => true],
]);
}
// Not setting the "framework.session.storage_factory_id" configuration option is deprecated in symfony 5.3
if (Kernel::VERSION_ID >= 50300) {
$container->loadFromExtension('framework', [
'session' => ['storage_factory_id' => 'session.storage.factory.mock_file'],
]);
} else {
$container->loadFromExtension('framework', [
'session' => ['storage_id' => 'session.storage.mock_file'],
]);
}
$container->addObjectResource($this);
});
}
/**
* (From MicroKernelTrait).
*
* @internal
*/
public function loadRoutes(LoaderInterface $loader)
{
return new RouteCollection();
}
/**
* {@inheritdoc}
*/
protected function buildContainer(): ContainerBuilder
{
$container = parent::buildContainer();
$container->addCompilerPass(new class() implements CompilerPassInterface {
public function process(ContainerBuilder $container)
{
foreach ($container->getDefinitions() as $id => $definition) {
if (preg_match('|Ekino.*|i', $id)) {
$definition->setPublic(true);
}
}
foreach ($container->getAliases() as $id => $alias) {
if (preg_match('|Ekino.*|i', $id)) {
$alias->setPublic(true);
}
}
}
});
return $container;
}
}
================================================
FILE: Tests/BundleInitializationTest.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Tests;
use Ekino\NewRelicBundle\EkinoNewRelicBundle;
use Ekino\NewRelicBundle\NewRelic\AdaptiveInteractor;
use Ekino\NewRelicBundle\NewRelic\BlackholeInteractor;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractor;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use PHPUnit\Framework\TestCase;
/**
* Smoke test to see if the bundle can run.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class BundleInitializationTest extends TestCase
{
protected function getBundleClass()
{
return EkinoNewRelicBundle::class;
}
public function testInitBundle()
{
$kernel = new AppKernel(uniqid('cache'));
$kernel->boot();
// Get the container
$container = $kernel->getContainer();
$services = [
NewRelicInteractorInterface::class => AdaptiveInteractor::class,
BlackholeInteractor::class,
NewRelicInteractor::class,
];
// Test if you services exists
foreach ($services as $id => $class) {
if (\is_int($id)) {
$id = $class;
}
$this->assertTrue($container->has($id));
$service = $container->get($id);
$this->assertInstanceOf($class, $service);
}
}
}
================================================
FILE: Tests/DependencyInjection/Compiler/MonologHandlerPassTest.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Tests\DependencyInjection\Compiler;
use Ekino\NewRelicBundle\DependencyInjection\Compiler\MonologHandlerPass;
use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractCompilerPassTestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class MonologHandlerPassTest extends AbstractCompilerPassTestCase
{
protected function registerCompilerPass(ContainerBuilder $container): void
{
$container->addCompilerPass(new MonologHandlerPass());
}
public function testProcessChannel()
{
$this->container->setParameter('ekino.new_relic.monolog', ['level' => 100, 'channels' => ['type' => 'inclusive', 'elements' => ['app', 'foo']]]);
$this->container->setParameter('ekino.new_relic.application_name', 'app');
$this->registerService('ekino.new_relic.monolog_handler', \Monolog\Handler\NewRelicHandler::class);
$this->container->setAlias('ekino.new_relic.logs_handler', 'ekino.new_relic.monolog_handler')->setPublic(false);
$this->registerService('monolog.logger', \Monolog\Logger::class)->setArgument(0, 'app');
$this->registerService('monolog.logger.foo', \Monolog\Logger::class)->setArgument(0, 'foo');
$this->compile();
$this->assertContainerBuilderHasServiceDefinitionWithMethodCall('monolog.logger', 'pushHandler', [new Reference('ekino.new_relic.logs_handler')]);
$this->assertContainerBuilderHasServiceDefinitionWithMethodCall('monolog.logger.foo', 'pushHandler', [new Reference('ekino.new_relic.logs_handler')]);
}
public function testProcessChannelAllChannels()
{
$this->container->setParameter('ekino.new_relic.monolog', ['level' => 100, 'channels' => null]);
$this->container->setParameter('ekino.new_relic.application_name', 'app');
$this->registerService('ekino.new_relic.monolog_handler', \Monolog\Handler\NewRelicHandler::class);
$this->container->setAlias('ekino.new_relic.logs_handler', 'ekino.new_relic.monolog_handler')->setPublic(false);
$this->registerService('monolog.logger', \Monolog\Logger::class)->setArgument(0, 'app');
$this->registerService('monolog.logger.foo', \Monolog\Logger::class)->setArgument(0, 'foo');
$this->compile();
$this->assertContainerBuilderHasServiceDefinitionWithMethodCall('monolog.logger', 'pushHandler', [new Reference('ekino.new_relic.logs_handler')]);
$this->assertContainerBuilderHasServiceDefinitionWithMethodCall('monolog.logger.foo', 'pushHandler', [new Reference('ekino.new_relic.logs_handler')]);
}
public function testProcessChannelExcludeChannels()
{
$this->container->setParameter('ekino.new_relic.monolog', ['level' => 100, 'channels' => ['type' => 'exclusive', 'elements' => ['foo']]]);
$this->container->setParameter('ekino.new_relic.application_name', 'app');
$this->registerService('ekino.new_relic.monolog_handler', \Monolog\Handler\NewRelicHandler::class);
$this->container->setAlias('ekino.new_relic.logs_handler', 'ekino.new_relic.monolog_handler')->setPublic(false);
$this->registerService('monolog.logger', \Monolog\Logger::class)->setArgument(0, 'app');
$this->registerService('monolog.logger.foo', \Monolog\Logger::class)->setArgument(0, 'foo');
$this->compile();
$this->assertContainerBuilderHasServiceDefinitionWithMethodCall('monolog.logger', 'pushHandler', [new Reference('ekino.new_relic.logs_handler')]);
}
}
================================================
FILE: Tests/DependencyInjection/ConfigurationTest.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Tests\DependencyInjection;
use Ekino\NewRelicBundle\DependencyInjection\Configuration;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\Config\Definition\PrototypedArrayNode;
class ConfigurationTest extends TestCase
{
public function testIgnoredRoutes()
{
$configuration = new Configuration();
$rootNode = $configuration->getConfigTreeBuilder()
->buildTree();
$children = $rootNode->getChildren();
/** @var PrototypedArrayNode $ignoredRoutesNode */
$ignoredRoutesNode = $children['http']->getChildren()['ignored_routes'];
$this->assertInstanceOf('\Symfony\Component\Config\Definition\PrototypedArrayNode', $ignoredRoutesNode);
$this->assertFalse($ignoredRoutesNode->isRequired());
$this->assertEmpty($ignoredRoutesNode->getDefaultValue());
$this->assertSame(['ignored_route1', 'ignored_route2'], $ignoredRoutesNode->normalize(['ignored_route1', 'ignored_route2']));
$this->assertSame(['ignored_route'], $ignoredRoutesNode->normalize('ignored_route'));
$this->assertSame(['ignored_route1', 'ignored_route2'], $ignoredRoutesNode->merge(['ignored_route1'], ['ignored_route2']));
}
public function testIgnoredPaths()
{
$configuration = new Configuration();
$rootNode = $configuration->getConfigTreeBuilder()
->buildTree();
$children = $rootNode->getChildren();
/** @var PrototypedArrayNode $ignoredPathsNode */
$ignoredPathsNode = $children['http']->getChildren()['ignored_paths'];
$this->assertInstanceOf('\Symfony\Component\Config\Definition\PrototypedArrayNode', $ignoredPathsNode);
$this->assertFalse($ignoredPathsNode->isRequired());
$this->assertEmpty($ignoredPathsNode->getDefaultValue());
$this->assertSame(['/ignored/path1', '/ignored/path2'], $ignoredPathsNode->normalize(['/ignored/path1', '/ignored/path2']));
$this->assertSame(['/ignored/path'], $ignoredPathsNode->normalize('/ignored/path'));
$this->assertSame(['/ignored/path1', '/ignored/path2'], $ignoredPathsNode->merge(['/ignored/path1'], ['/ignored/path2']));
}
public function testIgnoredCommands()
{
$configuration = new Configuration();
$rootNode = $configuration->getConfigTreeBuilder()
->buildTree();
$children = $rootNode->getChildren();
/** @var PrototypedArrayNode $ignoredCommandsNode */
$ignoredCommandsNode = $children['commands']->getChildren()['ignored_commands'];
$this->assertInstanceOf('\Symfony\Component\Config\Definition\PrototypedArrayNode', $ignoredCommandsNode);
$this->assertFalse($ignoredCommandsNode->isRequired());
$this->assertEmpty($ignoredCommandsNode->getDefaultValue());
$this->assertSame(['test:ignored-command1', 'test:ignored-command2'], $ignoredCommandsNode->normalize(['test:ignored-command1', 'test:ignored-command2']));
$this->assertSame(['test:ignored-command'], $ignoredCommandsNode->normalize('test:ignored-command'));
$this->assertSame(['test:ignored-command1', 'test:ignored-command2'], $ignoredCommandsNode->merge(['test:ignored-command1'], ['test:ignored-command2']));
}
public function testDefaults()
{
$processor = new Processor();
$config = $processor->processConfiguration(new Configuration(), []);
$this->assertEmpty($config['http']['ignored_routes']);
$this->assertIsArray($config['http']['ignored_routes']);
$this->assertEmpty($config['http']['ignored_paths']);
$this->assertIsArray($config['http']['ignored_paths']);
$this->assertEmpty($config['commands']['ignored_commands']);
$this->assertIsArray($config['commands']['ignored_commands']);
$this->assertEmpty($config['deployment_names']);
$this->assertIsArray($config['deployment_names']);
}
public static function ignoredRoutesProvider()
{
return [
['single_ignored_route', ['single_ignored_route']],
[['single_ignored_route'], ['single_ignored_route']],
[['ignored_route1', 'ignored_route2'], ['ignored_route1', 'ignored_route2']],
];
}
public static function ignoredPathsProvider()
{
return [
['/single/ignored/path', ['/single/ignored/path']],
[['/single/ignored/path'], ['/single/ignored/path']],
[['/ignored/path1', '/ignored/path2'], ['/ignored/path1', '/ignored/path2']],
];
}
public static function ignoredCommandsProvider()
{
return [
['single:ignored:command', ['single:ignored:command']],
[['single:ignored:command'], ['single:ignored:command']],
[['ignored:command1', 'ignored:command2'], ['ignored:command1', 'ignored:command2']],
];
}
public static function deploymentNamesProvider()
{
return [
['App1', ['App1']],
[['App1'], ['App1']],
[['App1', 'App2'], ['App1', 'App2']],
];
}
/**
* @dataProvider deploymentNamesProvider
*/
public function testDeploymentNames($deploymentNameConfig, $expected)
{
$processor = new Processor();
$config1 = $processor->processConfiguration(new Configuration(), ['ekino_new_relic' => ['deployment_name' => $deploymentNameConfig]]);
$config2 = $processor->processConfiguration(new Configuration(), ['ekino_new_relic' => ['deployment_names' => $deploymentNameConfig]]);
$this->assertSame($expected, $config1['deployment_names']);
$this->assertSame($expected, $config2['deployment_names']);
}
/**
* @dataProvider ignoredRoutesProvider
*/
public function testIgnoreRoutes($ignoredRoutesConfig, $expected)
{
$processor = new Processor();
$config = $processor->processConfiguration(new Configuration(), ['ekino_new_relic' => ['http' => ['ignored_routes' => $ignoredRoutesConfig]]]);
$this->assertSame($expected, $config['http']['ignored_routes']);
}
/**
* @dataProvider ignoredPathsProvider
*/
public function testIgnorePaths($ignoredPathsConfig, $expected)
{
$processor = new Processor();
$config = $processor->processConfiguration(new Configuration(), ['ekino_new_relic' => ['http' => ['ignored_paths' => $ignoredPathsConfig]]]);
$this->assertSame($expected, $config['http']['ignored_paths']);
}
/**
* @dataProvider ignoredCommandsProvider
*/
public function testIgnoreCommands($ignoredCommandsConfig, $expected)
{
$processor = new Processor();
$config = $processor->processConfiguration(new Configuration(), ['ekino_new_relic' => ['commands' => ['ignored_commands' => $ignoredCommandsConfig]]]);
$this->assertSame($expected, $config['commands']['ignored_commands']);
}
}
================================================
FILE: Tests/DependencyInjection/EkinoNewRelicExtensionTest.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Tests\DependencyInjection;
use Ekino\NewRelicBundle\DependencyInjection\EkinoNewRelicExtension;
use Ekino\NewRelicBundle\Listener\CommandListener;
use Ekino\NewRelicBundle\Listener\DeprecationListener;
use Ekino\NewRelicBundle\Listener\ExceptionListener;
use Ekino\NewRelicBundle\NewRelic\BlackholeInteractor;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use Ekino\NewRelicBundle\Twig\NewRelicExtension;
use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase;
use Matthias\SymfonyDependencyInjectionTest\PhpUnit\ContainerHasParameterConstraint;
use PHPUnit\Framework\Constraint\LogicalNot;
class EkinoNewRelicExtensionTest extends AbstractExtensionTestCase
{
protected function getContainerExtensions(): array
{
return [new EkinoNewRelicExtension()];
}
protected function setUp(): void
{
parent::setUp();
$this->setParameter('kernel.bundles', []);
}
public function testDefaultConfiguration()
{
$this->load();
$this->assertContainerBuilderHasService(NewRelicExtension::class);
$this->assertContainerBuilderHasService(CommandListener::class);
$this->assertContainerBuilderHasService(ExceptionListener::class);
}
public function testAlternativeConfiguration()
{
$this->load([
'exceptions' => false,
'commands' => false,
'twig' => false,
]);
$this->assertContainerBuilderNotHasService(NewRelicExtension::class);
$this->assertContainerBuilderNotHasService(CommandListener::class);
$this->assertContainerBuilderNotHasService(ExceptionListener::class);
}
public function testDeprecation()
{
$this->load();
$this->assertContainerBuilderHasService(DeprecationListener::class);
}
public function testMonolog()
{
$this->load(['monolog' => true]);
$this->assertContainerBuilderHasParameter('ekino.new_relic.monolog');
$this->assertContainerBuilderHasParameter('ekino.new_relic.application_name');
$this->assertContainerBuilderHasService('ekino.new_relic.logs_handler');
}
public function testMonologDisabled()
{
$this->load(['monolog' => false]);
self::assertThat(
$this->container,
new LogicalNot(new ContainerHasParameterConstraint('ekino.new_relic.monolog', null, false))
);
}
public function testConfigDisabled()
{
$this->load([
'enabled' => false,
]);
$this->assertContainerBuilderHasAlias(NewRelicInteractorInterface::class, BlackholeInteractor::class);
}
public function testConfigDisabledWithInteractor()
{
$this->load([
'enabled' => false,
'interactor' => 'ekino.new_relic.interactor.adaptive',
]);
$this->assertContainerBuilderHasAlias(NewRelicInteractorInterface::class, BlackholeInteractor::class);
}
public function testConfigEnabledWithInteractor()
{
$this->load([
'enabled' => true,
'interactor' => 'ekino.new_relic.interactor.adaptive',
]);
$this->assertContainerBuilderHasAlias(NewRelicInteractorInterface::class, 'ekino.new_relic.interactor.adaptive');
}
}
================================================
FILE: Tests/Listener/CommandListenerTest.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Tests\Listener;
use Ekino\NewRelicBundle\Listener\CommandListener;
use Ekino\NewRelicBundle\NewRelic\Config;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class CommandListenerTest extends TestCase
{
public function testCommandMarkedAsBackgroundJob()
{
if (!class_exists('Symfony\Component\Console\Event\ConsoleCommandEvent')) {
$this->markTestSkipped('Console Events is only available from Symfony 2.3');
}
$parameters = [
'--foo' => true,
'--foobar' => ['baz', 'baz_2'],
'name' => 'bar',
];
$definition = new InputDefinition([
new InputOption('foo'),
new InputOption('foobar', 'fb', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY),
new InputArgument('name', InputArgument::REQUIRED),
]);
$interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
$interactor->expects($this->once())->method('setTransactionName')->with($this->equalTo('test:newrelic'));
$interactor->expects($this->once())->method('enableBackgroundJob');
$interactor->expects($this->exactly(4))->method('addCustomParameter')->withConsecutive(
['--foo', true],
['--foobar[0]', 'baz'],
['--foobar[1]', 'baz_2'],
['name', 'bar']
);
$command = new Command('test:newrelic');
$input = new ArrayInput($parameters, $definition);
$output = $this->getMockBuilder(OutputInterface::class)->getMock();
$event = new ConsoleCommandEvent($command, $input, $output);
$listener = new CommandListener(new Config('App name', 'Token'), $interactor, []);
$listener->onConsoleCommand($event);
}
public function testIgnoreBackgroundJob()
{
$interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
$interactor->expects($this->never())->method('startTransaction');
$command = new Command('test:ignored-commnand');
$input = new ArrayInput([], new InputDefinition([]));
$output = $this->getMockBuilder(OutputInterface::class)->getMock();
$event = new ConsoleCommandEvent($command, $input, $output);
$listener = new CommandListener(new Config('App name', 'Token'), $interactor, ['test:ignored-command']);
$listener->onConsoleCommand($event);
}
public function testConsoleError()
{
$exception = new \Exception('', 1);
$newrelic = $this->getMockBuilder(Config::class)->disableOriginalConstructor()->getMock();
$interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
$interactor->expects($this->once())->method('noticeThrowable')->with($exception);
$command = new Command('test:exception');
$input = new ArrayInput([], new InputDefinition([]));
$output = $this->getMockBuilder(OutputInterface::class)->getMock();
$event = new ConsoleErrorEvent($input, $output, $exception, $command);
$listener = new CommandListener($newrelic, $interactor, ['test:exception']);
$listener->onConsoleError($event);
}
public function testConsoleErrorsWithThrowable()
{
$exception = new \Error();
$newrelic = $this->getMockBuilder(Config::class)->disableOriginalConstructor()->getMock();
$interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
$interactor->expects($this->once())->method('noticeThrowable')->with($exception);
$command = new Command('test:exception');
$input = new ArrayInput([], new InputDefinition([]));
$output = $this->getMockBuilder(OutputInterface::class)->getMock();
$event = new ConsoleErrorEvent($input, $output, $exception, $command);
$listener = new CommandListener($newrelic, $interactor, ['test:exception']);
$listener->onConsoleError($event);
}
}
================================================
FILE: Tests/Listener/DeprecationListenerTest.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Tests\Listener;
use Ekino\NewRelicBundle\Exception\DeprecationException;
use Ekino\NewRelicBundle\Listener\DeprecationListener;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use PHPUnit\Framework\TestCase;
class DeprecationListenerTest extends TestCase
{
public function testDeprecationIsReported()
{
$interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
$interactor->expects($this->once())->method('noticeThrowable')->with(
$this->isInstanceOf(DeprecationException::class)
);
$listener = new DeprecationListener($interactor);
set_error_handler(function () { return false; });
try {
$listener->register();
@trigger_error('This is a deprecation', \E_USER_DEPRECATED);
} finally {
$listener->unregister();
restore_error_handler();
}
}
public function testDeprecationIsReportedRegardlessErrorReporting()
{
$interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
$interactor->expects($this->once())->method('noticeThrowable');
$listener = new DeprecationListener($interactor);
set_error_handler(function () { return false; });
$e = error_reporting(0);
try {
$listener->register();
@trigger_error('This is a deprecation', \E_USER_DEPRECATED);
} finally {
$listener->unregister();
error_reporting($e);
restore_error_handler();
}
}
public function testOtherErrorAreIgnored()
{
$interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
$interactor->expects($this->never())->method('noticeThrowable');
$listener = new DeprecationListener($interactor);
set_error_handler(function () { return false; });
try {
$listener->register();
@trigger_error('This is a notice', \E_USER_NOTICE);
} finally {
$listener->unregister();
restore_error_handler();
}
}
public function testInitialHandlerIsCalled()
{
$interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
$interactor->expects($this->once())->method('noticeThrowable');
$handler = $this->createPartialMock(DummyHandler::class, ['__invoke']);
$handler->expects($this->once())->method('__invoke');
$listener = new DeprecationListener($interactor);
set_error_handler($handler);
try {
$listener->register();
@trigger_error('This is a deprecation', \E_USER_DEPRECATED);
} finally {
$listener->unregister();
restore_error_handler();
}
}
public function testUnregisterRemovesHandler()
{
$interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
$interactor->expects($this->never())->method('noticeThrowable');
$listener = new DeprecationListener($interactor);
set_error_handler(function () { return false; });
try {
$listener->register();
$listener->unregister();
@trigger_error('This is a deprecation', \E_USER_DEPRECATED);
} finally {
restore_error_handler();
}
}
public function testUnregisterRestorePreviousHandler()
{
$interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
$handler = $this->createPartialMock(DummyHandler::class, ['__invoke']);
$handler->expects($this->once())->method('__invoke');
$listener = new DeprecationListener($interactor);
set_error_handler($handler);
try {
$listener->register();
$listener->unregister();
@trigger_error('This is a deprecation', \E_USER_DEPRECATED);
} finally {
restore_error_handler();
}
}
}
class DummyHandler
{
public function __invoke()
{
}
}
================================================
FILE: Tests/Listener/ExceptionListenerTest.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Tests\Listener;
use Ekino\NewRelicBundle\Listener\ExceptionListener;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class ExceptionListenerTest extends TestCase
{
public function testOnKernelException()
{
$exception = new \Exception('Boom');
$interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
$interactor->expects($this->once())->method('noticeThrowable')->with($exception);
$kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();
$request = new Request();
$eventClass = class_exists(ExceptionEvent::class) ? ExceptionEvent::class : GetResponseForExceptionEvent::class;
$event = new $eventClass($kernel, $request, HttpKernelInterface::SUB_REQUEST, $exception);
$listener = new ExceptionListener($interactor);
$listener->onKernelException($event);
}
public function testOnKernelExceptionWithHttp()
{
$exception = new BadRequestHttpException('Boom');
$interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
$interactor->expects($this->never())->method('noticeThrowable');
$kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();
$request = new Request();
$eventClass = class_exists(ExceptionEvent::class) ? ExceptionEvent::class : GetResponseForExceptionEvent::class;
$event = new $eventClass($kernel, $request, HttpKernelInterface::SUB_REQUEST, $exception);
$listener = new ExceptionListener($interactor);
$listener->onKernelException($event);
}
}
================================================
FILE: Tests/Listener/RequestListenerTest.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Tests\Listener;
use Ekino\NewRelicBundle\Listener\RequestListener;
use Ekino\NewRelicBundle\NewRelic\Config;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use Ekino\NewRelicBundle\TransactionNamingStrategy\TransactionNamingStrategyInterface;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class RequestListenerTest extends TestCase
{
public function testSubRequest()
{
$interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
$interactor->expects($this->never())->method('setTransactionName');
$namingStrategy = $this->getMockBuilder(TransactionNamingStrategyInterface::class)->getMock();
$kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();
$eventClass = class_exists(RequestEvent::class) ? RequestEvent::class : GetResponseEvent::class;
$event = new $eventClass($kernel, new Request(), HttpKernelInterface::SUB_REQUEST, new Response());
$listener = new RequestListener(new Config('App name', 'Token'), $interactor, [], [], $namingStrategy);
$listener->setApplicationName($event);
}
public function testMasterRequest()
{
$interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
$interactor->expects($this->once())->method('setTransactionName');
$namingStrategy = $this->getMockBuilder(TransactionNamingStrategyInterface::class)
->setMethods(['getTransactionName'])
->getMock();
$namingStrategy->expects($this->once())->method('getTransactionName')->willReturn('foobar');
$kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();
$eventClass = class_exists(RequestEvent::class) ? RequestEvent::class : GetResponseEvent::class;
$event = new $eventClass($kernel, new Request(), HttpKernelInterface::MASTER_REQUEST, new Response());
$listener = new RequestListener(new Config('App name', 'Token'), $interactor, [], [], $namingStrategy);
$listener->setTransactionName($event);
}
public function testPathIsIgnored()
{
$interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
$interactor->expects($this->once())->method('ignoreTransaction');
$namingStrategy = $this->getMockBuilder(TransactionNamingStrategyInterface::class)->getMock();
$kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();
$request = new Request([], [], [], [], [], ['REQUEST_URI' => '/ignored_path']);
$eventClass = class_exists(RequestEvent::class) ? RequestEvent::class : GetResponseEvent::class;
$event = new $eventClass($kernel, $request, HttpKernelInterface::MASTER_REQUEST, new Response());
$listener = new RequestListener(new Config('App name', 'Token'), $interactor, [], ['/ignored_path'], $namingStrategy);
$listener->setIgnoreTransaction($event);
}
public function testRouteIsIgnored()
{
$interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
$interactor->expects($this->once())->method('ignoreTransaction');
$namingStrategy = $this->getMockBuilder(TransactionNamingStrategyInterface::class)->getMock();
$kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();
$request = new Request([], [], ['_route' => 'ignored_route']);
$eventClass = class_exists(RequestEvent::class) ? RequestEvent::class : GetResponseEvent::class;
$event = new $eventClass($kernel, $request, HttpKernelInterface::MASTER_REQUEST, new Response());
$listener = new RequestListener(new Config('App name', 'Token'), $interactor, ['ignored_route'], [], $namingStrategy);
$listener->setIgnoreTransaction($event);
}
public function testSymfonyCacheEnabled()
{
$interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
$interactor->expects($this->once())->method('startTransaction');
$namingStrategy = $this->getMockBuilder(TransactionNamingStrategyInterface::class)->getMock();
$kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();
$eventClass = class_exists(RequestEvent::class) ? RequestEvent::class : GetResponseEvent::class;
$event = new $eventClass($kernel, new Request(), HttpKernelInterface::MASTER_REQUEST, new Response());
$listener = new RequestListener(new Config('App name', 'Token'), $interactor, [], [], $namingStrategy, true);
$listener->setApplicationName($event);
}
public function testSymfonyCacheDisabled()
{
$interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
$interactor->expects($this->never())->method('startTransaction');
$namingStrategy = $this->getMockBuilder(TransactionNamingStrategyInterface::class)->getMock();
$kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();
$eventClass = class_exists(RequestEvent::class) ? RequestEvent::class : GetResponseEvent::class;
$event = new $eventClass($kernel, new Request(), HttpKernelInterface::MASTER_REQUEST, new Response());
$listener = new RequestListener(new Config('App name', 'Token'), $interactor, [], [], $namingStrategy, false);
$listener->setApplicationName($event);
}
}
================================================
FILE: Tests/Listener/ResponseListenerTest.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Tests\Listener;
use Ekino\NewRelicBundle\Listener\ResponseListener;
use Ekino\NewRelicBundle\NewRelic\Config;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use Ekino\NewRelicBundle\Twig\NewRelicExtension;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class ResponseListenerTest extends TestCase
{
protected function setUp(): void
{
$this->interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
$this->newRelic = $this->getMockBuilder(Config::class)
->setMethods(['getCustomEvents', 'getCustomMetrics', 'getCustomParameters'])
->disableOriginalConstructor()
->getMock();
$this->extension = $this->getMockBuilder(NewRelicExtension::class)
->setMethods(['isHeaderCalled', 'isFooterCalled', 'isUsed'])
->disableOriginalConstructor()
->getMock();
}
public function testOnKernelResponseOnlyMasterRequestsAreProcessed()
{
$event = $this->createFilterResponseEventDummy(null, null, HttpKernelInterface::SUB_REQUEST);
$object = new ResponseListener($this->newRelic, $this->interactor);
$object->onKernelResponse($event);
$this->newRelic->expects($this->never())->method('getCustomMetrics');
}
public function testOnKernelResponseWithOnlyCustomMetricsAndParameters()
{
$events = [
'WidgetSale' => [
[
'color' => 'red',
'weight' => 12.5,
],
[
'color' => 'blue',
'weight' => 12.5,
],
],
];
$metrics = [
'foo_a' => 4.7,
'foo_b' => 11,
];
$parameters = [
'foo_1' => 'bar_1',
'foo_2' => 'bar_2',
];
$this->newRelic->expects($this->once())->method('getCustomEvents')->willReturn($events);
$this->newRelic->expects($this->once())->method('getCustomMetrics')->willReturn($metrics);
$this->newRelic->expects($this->once())->method('getCustomParameters')->willReturn($parameters);
$this->interactor->expects($this->exactly(2))->method('addCustomMetric')->withConsecutive(
['foo_a', 4.7],
['foo_b', 11]
);
$this->interactor->expects($this->exactly(2))->method('addCustomParameter')->withConsecutive(
['foo_1', 'bar_1'],
['foo_2', 'bar_2']
);
$this->interactor->expects($this->exactly(2))->method('addCustomEvent')->withConsecutive(
['WidgetSale', [
'color' => 'red',
'weight' => 12.5,
]],
['WidgetSale', [
'color' => 'blue',
'weight' => 12.5,
]]
);
$event = $this->createFilterResponseEventDummy();
$object = new ResponseListener($this->newRelic, $this->interactor, false);
$object->onKernelResponse($event);
}
public function testOnKernelResponseInstrumentDisabledInRequest()
{
$this->setupNoCustomMetricsOrParameters();
$this->interactor->expects($this->once())->method('disableAutoRUM');
$event = $this->createFilterResponseEventDummy();
$object = new ResponseListener($this->newRelic, $this->interactor, true);
$object->onKernelResponse($event);
}
public function testSymfonyCacheEnabled()
{
$this->setupNoCustomMetricsOrParameters();
$this->interactor->expects($this->once())->method('endTransaction');
$event = $this->createFilterResponseEventDummy();
$object = new ResponseListener($this->newRelic, $this->interactor, false, true);
$object->onKernelResponse($event);
}
public function testSymfonyCacheDisabled()
{
$this->setupNoCustomMetricsOrParameters();
$this->interactor->expects($this->never())->method('endTransaction');
$event = $this->createFilterResponseEventDummy();
$object = new ResponseListener($this->newRelic, $this->interactor, false, false);
$object->onKernelResponse($event);
}
/**
* @dataProvider providerOnKernelResponseOnlyInstrumentHTMLResponses
*/
public function testOnKernelResponseOnlyInstrumentHTMLResponses($content, $expectsSetContent, $contentType)
{
$this->setupNoCustomMetricsOrParameters();
$this->interactor->expects($this->once())->method('disableAutoRUM');
$this->interactor->expects($this->any())->method('getBrowserTimingHeader')->willReturn('__Timing_Header__');
$this->interactor->expects($this->any())->method('getBrowserTimingFooter')->willReturn('__Timing_Feader__');
$response = $this->createResponseMock($content, $expectsSetContent, $contentType);
$event = $this->createFilterResponseEventDummy(null, $response);
$object = new ResponseListener($this->newRelic, $this->interactor, true);
$object->onKernelResponse($event);
}
public function providerOnKernelResponseOnlyInstrumentHTMLResponses()
{
return [
// unsupported content types
[null, null, 'text/xml'],
[null, null, 'text/plain'],
[null, null, 'application/json'],
['content', 'content', 'text/html'],
['<div class="head">head</div>', '<div class="head">head</div>', 'text/html'],
['<header>content</header>', '<header>content</header>', 'text/html'],
// head, body tags
['<head><title /></head>', '<head>__Timing_Header__<title /></head>', 'text/html'],
['<body><div /></body>', '<body><div />__Timing_Feader__</body>', 'text/html'],
['<head><title /></head><body><div /></body>', '<head>__Timing_Header__<title /></head><body><div />__Timing_Feader__</body>', 'text/html'],
// with charset
['<head><title /></head><body><div /></body>', '<head>__Timing_Header__<title /></head><body><div />__Timing_Feader__</body>', 'text/html; charset=UTF-8'],
];
}
public function testInteractionWithTwigExtensionHeader()
{
$this->newRelic->expects($this->never())->method('getCustomMetrics');
$this->newRelic->expects($this->never())->method('getCustomParameters');
$this->newRelic->expects($this->once())->method('getCustomEvents')->willReturn([]);
$this->interactor->expects($this->never())->method('disableAutoRUM');
$this->interactor->expects($this->never())->method('getBrowserTimingHeader');
$this->interactor->expects($this->once())->method('getBrowserTimingFooter')->willReturn('__Timing_Feader__');
$this->extension->expects($this->exactly(2))->method('isUsed')->willReturn(true);
$this->extension->expects($this->once())->method('isHeaderCalled')->willReturn(true);
$this->extension->expects($this->once())->method('isFooterCalled')->willReturn(false);
$request = $this->createRequestMock(true);
$response = $this->createResponseMock('content', 'content', 'text/html');
$event = $this->createFilterResponseEventDummy($request, $response);
$object = new ResponseListener($this->newRelic, $this->interactor, true, false, $this->extension);
$object->onKernelResponse($event);
}
public function testInteractionWithTwigExtensionFooter()
{
$this->newRelic->expects($this->never())->method('getCustomMetrics');
$this->newRelic->expects($this->never())->method('getCustomParameters');
$this->newRelic->expects($this->once())->method('getCustomEvents')->willReturn([]);
$this->interactor->expects($this->never())->method('disableAutoRUM');
$this->interactor->expects($this->once())->method('getBrowserTimingHeader')->willReturn('__Timing_Feader__');
$this->interactor->expects($this->never())->method('getBrowserTimingFooter');
$this->extension->expects($this->exactly(2))->method('isUsed')->willReturn(true);
$this->extension->expects($this->once())->method('isHeaderCalled')->willReturn(false);
$this->extension->expects($this->once())->method('isFooterCalled')->willReturn(true);
$request = $this->createRequestMock(true);
$response = $this->createResponseMock('content', 'content', 'text/html');
$event = $this->createFilterResponseEventDummy($request, $response);
$object = new ResponseListener($this->newRelic, $this->interactor, true, false, $this->extension);
$object->onKernelResponse($event);
}
public function testInteractionWithTwigExtensionHeaderFooter()
{
$this->newRelic->expects($this->never())->method('getCustomMetrics');
$this->newRelic->expects($this->never())->method('getCustomParameters');
$this->newRelic->expects($this->once())->method('getCustomEvents')->willReturn([]);
$this->interactor->expects($this->never())->method('disableAutoRUM');
$this->interactor->expects($this->never())->method('getBrowserTimingHeader');
$this->interactor->expects($this->never())->method('getBrowserTimingFooter');
$this->extension->expects($this->exactly(2))->method('isUsed')->willReturn(true);
$this->extension->expects($this->once())->method('isHeaderCalled')->willReturn(true);
$this->extension->expects($this->once())->method('isFooterCalled')->willReturn(true);
$request = $this->createRequestMock(true);
$response = $this->createResponseMock('content', 'content', 'text/html');
$event = $this->createFilterResponseEventDummy($request, $response);
$object = new ResponseListener($this->newRelic, $this->interactor, true, false, $this->extension);
$object->onKernelResponse($event);
}
private function setUpNoCustomMetricsOrParameters()
{
$this->newRelic->expects($this->once())->method('getCustomEvents')->willReturn([]);
$this->newRelic->expects($this->once())->method('getCustomMetrics')->willReturn([]);
$this->newRelic->expects($this->once())->method('getCustomParameters')->willReturn([]);
$this->interactor->expects($this->never())->method('addCustomEvent');
$this->interactor->expects($this->never())->method('addCustomMetric');
$this->interactor->expects($this->never())->method('addCustomParameter');
}
private function createRequestMock($instrumentEnabled = true)
{
$mock = $this->getMockBuilder(Request::class)
->setMethods(['get'])
->getMock();
$mock->attributes = $mock;
$mock->expects($this->any())->method('get')->willReturn($instrumentEnabled);
return $mock;
}
private function createResponseMock($content = null, $expectsSetContent = null, $contentType = 'text/html')
{
$mock = $this->getMockBuilder(Response::class)
->setMethods(['get', 'getContent', 'setContent'])
->getMock();
$mock->headers = $mock;
$mock->expects($this->any())->method('get')->willReturn($contentType);
$mock->expects($content ? $this->any() : $this->never())->method('getContent')->willReturn($content ?? false);
if ($expectsSetContent) {
$mock->expects($this->exactly(2))->method('setContent')->withConsecutive([''], [$expectsSetContent]);
} else {
$mock->expects($this->never())->method('setContent');
}
return $mock;
}
private function createFilterResponseEventDummy(Request $request = null, Response $response = null, int $requestType = HttpKernelInterface::MASTER_REQUEST)
{
$kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();
$eventClass = class_exists(ResponseEvent::class) ? ResponseEvent::class : FilterResponseEvent::class;
$event = new $eventClass($kernel, $request ?? new Request(), $requestType, $response ?? new Response());
return $event;
}
}
================================================
FILE: Tests/NewRelic/ConfigTest.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Tests\NewRelic;
use Ekino\NewRelicBundle\NewRelic\Config;
use PHPUnit\Framework\TestCase;
class ConfigTest extends TestCase
{
public function testGeneric()
{
$newRelic = new Config('Ekino', 'XXX', null, false, [], 'api.host');
$this->assertSame('Ekino', $newRelic->getName());
$this->assertSame('XXX', $newRelic->getApiKey());
$this->assertSame('api.host', $newRelic->getApiHost());
$this->assertEmpty($newRelic->getCustomEvents());
$this->assertEmpty($newRelic->getCustomMetrics());
$this->assertEmpty($newRelic->getCustomParameters());
$newRelic->addCustomEvent('WidgetSale', ['color' => 'red', 'weight' => 12.5]);
$newRelic->addCustomEvent('WidgetSale', ['color' => 'blue', 'weight' => 12.5]);
$expected = [
'WidgetSale' => [
[
'color' => 'red',
'weight' => 12.5,
],
[
'color' => 'blue',
'weight' => 12.5,
],
],
];
$this->assertSame($expected, $newRelic->getCustomEvents());
$newRelic->addCustomMetric('foo', 4.2);
$newRelic->addCustomMetric('asd', 1);
$expected = [
'foo' => 4.2,
'asd' => 1.0,
];
$this->assertSame($expected, $newRelic->getCustomMetrics());
$newRelic->addCustomParameter('param1', 1);
$expected = [
'param1' => 1,
];
$this->assertSame($expected, $newRelic->getCustomParameters());
}
public function testDefaults()
{
$newRelic = new Config('', '');
$this->assertNotNull($newRelic->getName());
$this->assertSame(ini_get('newrelic.appname') ?: '', $newRelic->getName());
$this->assertNotNull($newRelic->getLicenseKey());
$this->assertSame(ini_get('newrelic.license') ?: '', $newRelic->getLicenseKey());
$this->assertNull($newRelic->getApiHost());
}
}
================================================
FILE: Tests/NewRelic/LoggingInteractorDecoratorTest.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Tests\NewRelic;
use Ekino\NewRelicBundle\NewRelic\LoggingInteractorDecorator;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class LoggingInteractorDecoratorTest extends TestCase
{
/**
* @dataProvider provideMethods
*/
public function testGeneric(string $method, array $arguments, $return)
{
$logger = $this->createMock(LoggerInterface::class);
$decorated = $this->createMock(LoggingInteractorDecorator::class);
$interactor = new LoggingInteractorDecorator($decorated, $logger);
$logger->expects($this->once())->method('debug');
$call = $decorated->expects($this->once())->method($method)
->with(...$arguments);
if (null !== $return) {
$call->willReturn($return);
}
$result = $interactor->$method(...$arguments);
$this->assertSame($return, $result);
}
public function provideMethods()
{
$reflection = new \ReflectionClass(NewRelicInteractorInterface::class);
foreach ($reflection->getMethods() as $method) {
if (!$method->isPublic()) {
continue;
}
if ($method->isStatic()) {
continue;
}
$arguments = array_map(function (\ReflectionParameter $parameter) {
return $this->getTypeStub($parameter->getType());
}, $method->getParameters());
$return = $method->hasReturnType() ? $this->getTypeStub($method->getReturnType()) : null;
yield [$method->getName(), $arguments, $return];
}
}
private function getTypeStub(?\ReflectionType $type)
{
if (null === $type) {
return uniqid('', true);
}
switch ($type->getName()) {
case 'string':
return uniqid('', true);
case 'bool':
return (bool) rand(0, 1);
case 'float':
return rand(0, 100) / rand(1, 10);
case 'int':
return rand(0, 100);
case 'void':
return null;
case 'Throwable':
return new \Exception();
case 'callable':
return function () {};
case 'array':
return array_fill(0, 2, uniqid('', true));
default:
throw new \UnexpectedValueException('Unknown type. '.$type->getName());
}
}
}
================================================
FILE: Tests/TransactionNamingStrategy/ControllerNamingStrategyTest.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Tests\TransactionNamingStrategy;
use Ekino\NewRelicBundle\TransactionNamingStrategy\ControllerNamingStrategy;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
class ControllerNamingStrategyTest extends TestCase
{
public function testControllerAsString()
{
$request = new Request();
$request->attributes->set('_controller', 'SomeBundle:Some:SomeAction');
$strategy = new ControllerNamingStrategy();
$this->assertSame('SomeBundle:Some:SomeAction', $strategy->getTransactionName($request));
}
public function testControllerAsClosure()
{
$request = new Request();
$request->attributes->set('_controller', function () {
});
$strategy = new ControllerNamingStrategy();
$this->assertSame('Closure controller', $strategy->getTransactionName($request));
}
public function testControllerAsCallback()
{
$request = new Request();
$request->attributes->set('_controller', [$this, 'testControllerAsString']);
$strategy = new ControllerNamingStrategy();
$this->assertSame('Callback controller: Ekino\NewRelicBundle\Tests\TransactionNamingStrategy\ControllerNamingStrategyTest::testControllerAsString()', $strategy->getTransactionName($request));
}
public function testControllerUnknown()
{
$request = new Request();
$strategy = new ControllerNamingStrategy();
$this->assertSame('Unknown Symfony controller', $strategy->getTransactionName($request));
}
}
================================================
FILE: Tests/Twig/NewRelicExtensionTest.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Tests\Twig;
use Ekino\NewRelicBundle\NewRelic\Config;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use Ekino\NewRelicBundle\Twig\NewRelicExtension;
use PHPUnit\Framework\TestCase;
class NewRelicExtensionTest extends TestCase
{
/**
* @var \Ekino\NewRelicBundle\NewRelic\Config
*/
private $newRelic;
/**
* @var \Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface
*/
private $interactor;
protected function setUp(): void
{
$this->newRelic = $this->getMockBuilder(Config::class)
->setMethods(['getCustomMetrics', 'getCustomParameters'])
->disableOriginalConstructor()
->getMock();
$this->interactor = $this->getMockBuilder(NewRelicInteractorInterface::class)->getMock();
}
/**
* Tests the initial values returned by state methods.
*/
public function testInitialSetup()
{
$extension = new NewRelicExtension(
$this->newRelic,
$this->interactor
);
$this->assertFalse($extension->isHeaderCalled());
$this->assertFalse($extension->isFooterCalled());
$this->assertFalse($extension->isUsed());
}
public function testHeaderException()
{
$extension = new NewRelicExtension(
$this->newRelic,
$this->interactor
);
$this->newRelic->expects($this->once())
->method('getCustomMetrics')
->willReturn([]);
$this->newRelic->expects($this->once())
->method('getCustomParameters')
->willReturn([]);
$this->expectException(\RuntimeException::class);
$extension->getNewrelicBrowserTimingHeader();
$extension->getNewrelicBrowserTimingHeader();
}
public function testFooterException()
{
$extension = new NewRelicExtension(
$this->newRelic,
$this->interactor
);
$this->newRelic->expects($this->once())
->method('getCustomMetrics')
->willReturn([]);
$this->newRelic->expects($this->once())
->method('getCustomParameters')
->willReturn([]);
$this->expectException(\RuntimeException::class);
$extension->getNewrelicBrowserTimingHeader();
$extension->getNewrelicBrowserTimingHeader();
}
public function testPreparingOfInteractor()
{
$headerValue = '__HEADER__TIMING__';
$footerValue = '__FOOTER__TIMING__';
$extension = new NewRelicExtension(
$this->newRelic,
$this->interactor,
true
);
$this->newRelic->expects($this->once())
->method('getCustomMetrics')
->willReturn([
'a' => 'b',
'c' => 'd',
]);
$this->newRelic->expects($this->once())
->method('getCustomParameters')
->willReturn([
'e' => 'f',
'g' => 'h',
'i' => 'j',
]);
$this->interactor->expects($this->once())
->method('disableAutoRum');
$this->interactor->expects($this->exactly(2))
->method('addCustomMetric');
$this->interactor->expects($this->exactly(3))
->method('addCustomParameter');
$this->interactor->expects($this->once())
->method('getBrowserTimingHeader')
->willReturn($headerValue);
$this->interactor->expects($this->once())
->method('getBrowserTimingFooter')
->willReturn($footerValue);
$this->assertSame($headerValue, $extension->getNewrelicBrowserTimingHeader());
$this->assertTrue($extension->isHeaderCalled());
$this->assertFalse($extension->isFooterCalled());
$this->assertSame($footerValue, $extension->getNewrelicBrowserTimingFooter());
$this->assertTrue($extension->isHeaderCalled());
$this->assertTrue($extension->isFooterCalled());
}
}
================================================
FILE: TransactionNamingStrategy/ControllerNamingStrategy.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\TransactionNamingStrategy;
use Symfony\Component\HttpFoundation\Request;
/**
* @author Magnus Nordlander
* @author Bart van den Burg <bart@burgov.nl>
*/
class ControllerNamingStrategy implements TransactionNamingStrategyInterface
{
public function getTransactionName(Request $request): string
{
$controller = $request->attributes->get('_controller');
if (empty($controller)) {
return 'Unknown Symfony controller';
}
if ($controller instanceof \Closure) {
return 'Closure controller';
}
if (\is_object($controller)) {
if (method_exists($controller, '__invoke')) {
return 'Callback controller: '.\get_class($controller).'::__invoke()';
}
}
if (\is_callable($controller)) {
if (\is_array($controller)) {
if (\is_object($controller[0])) {
$controller[0] = \get_class($controller[0]);
}
$controller = implode('::', $controller);
}
return 'Callback controller: '.$controller.'()';
}
return $controller;
}
}
================================================
FILE: TransactionNamingStrategy/RouteNamingStrategy.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\TransactionNamingStrategy;
use Symfony\Component\HttpFoundation\Request;
/**
* @author Magnus Nordlander
*/
class RouteNamingStrategy implements TransactionNamingStrategyInterface
{
public function getTransactionName(Request $request): string
{
return $request->get('_route') ?: 'Unknown Symfony route';
}
}
================================================
FILE: TransactionNamingStrategy/TransactionNamingStrategyInterface.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\TransactionNamingStrategy;
use Symfony\Component\HttpFoundation\Request;
interface TransactionNamingStrategyInterface
{
public function getTransactionName(Request $request): string;
}
================================================
FILE: Twig/NewRelicExtension.php
================================================
<?php
declare(strict_types=1);
/*
* This file is part of Ekino New Relic bundle.
*
* (c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Ekino\NewRelicBundle\Twig;
use Ekino\NewRelicBundle\NewRelic\Config;
use Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
/**
* Twig extension to manually include BrowserTimingHeader and BrowserTimingFooter into twig templates.
*/
class NewRelicExtension extends AbstractExtension
{
private $newRelic;
private $interactor;
private $instrument;
private $headerCalled = false;
private $footerCalled = false;
public function __construct(
Config $newRelic,
NewRelicInteractorInterface $interactor,
bool $instrument = false
) {
$this->newRelic = $newRelic;
$this->interactor = $interactor;
$this->instrument = $instrument;
}
public function getFunctions(): array
{
return [
new TwigFunction('ekino_newrelic_browser_timing_header', [$this, 'getNewrelicBrowserTimingHeader'], ['is_safe' => ['html']]),
new TwigFunction('ekino_newrelic_browser_timing_footer', [$this, 'getNewrelicBrowserTimingFooter'], ['is_safe' => ['html']]),
];
}
/**
* @throws \RuntimeException
*/
public function getNewrelicBrowserTimingHeader(): string
{
if ($this->isHeaderCalled()) {
throw new \RuntimeException('Function "ekino_newrelic_browser_timing_header" has already been called');
}
$this->prepareInteractor();
$this->headerCalled = true;
return $this->interactor->getBrowserTimingHeader();
}
/**
* @throws \RuntimeException
*/
public function getNewrelicBrowserTimingFooter(): string
{
if ($this->isFooterCalled()) {
throw new \RuntimeException('Function "ekino_newrelic_browser_timing_footer" has already been called');
}
if (false === $this->isHeaderCalled()) {
$this->prepareInteractor();
}
$this->footerCalled = true;
return $this->interactor->getBrowserTimingFooter();
}
public function isHeaderCalled(): bool
{
return $this->headerCalled;
}
public function isFooterCalled(): bool
{
return $this->footerCalled;
}
public function isUsed(): bool
{
return $this->isHeaderCalled() || $this->isFooterCalled();
}
private function prepareInteractor(): void
{
if ($this->instrument) {
$this->interactor->disableAutoRUM();
}
foreach ($this->newRelic->getCustomMetrics() as $name => $value) {
$this->interactor->addCustomMetric((string) $name, (float) $value);
}
foreach ($this->newRelic->getCustomParameters() as $name => $value) {
$this->interactor->addCustomParameter((string) $name, $value);
}
}
}
================================================
FILE: UPGRADE-2.0.md
================================================
UPGRADE FROM 1.x to 2.0
=======================
> Many internal things in the bundle has changed. If you only installed the
> bundle and added some configuration then you just have to check the 2 first
> bullets of this file.
* The namespace of the bundle has changed to follow:
Before:
```php
$bundles[] = new Ekino\Bundle\NewRelicBundle\EkinoNewRelicBundle();
```
After:
```php
$bundles[] = new Ekino\NewRelicBundle\EkinoNewRelicBundle();
```
* The configuration structure has changed
```
| EkinoNewRelicBundle 1.x | EkinoNewRelicBundle 2.0
| ---------------------------- | --------------------------------
| ekino_new_relic: | ekino_new_relic:
| enabled | enabled
| application_name | application_name
| deployment_names | deployment_names
| api_key | api_key
| license_key | license_key
| xmit | xmit
| logging | logging
| instrument | instrument
| log_exceptions | exceptions
| | interactor
| | twig
| | deprecations
| | http
| | enabled
| using_symfony_cache | using_symfony_cache
| transaction_naming | transaction_naming
| transaction_naming_service | transaction_naming_service
| ignored_routes | ignored_routes
| ignored_paths | ignored_paths
| | monolog
| | enabled
| | channels
| | level
| | service
| | commands
| log_commands | enabled
| ignored_commands | ignored_commands
```
* The Sonata integration has been removed.
* The Silex integration has been removed.
* Services are private by default. You should either use service injection
or explicitly define your services as public if you really need to inject
the container.
* The parameters `ekino.new_relic.interactor.real.class` and `ekino.new_relic.interactor.blackhole.class`
have been removed. You should decorate the services instead.
* Name of services uses the class FQDN instead of string alias
| EkinoNewRelicBundle 1.x | EkinoNewRelicBundle 2.0
| -------------------------------------------------------- | --------------------------------------------------------------------------
| `ekino.new_relic.command_listener` | `Ekino\NewRelicBundle\Listener\CommandListener`
| `ekino.new_relic.exception_listener` | `Ekino\NewRelicBundle\Listener\ExceptionListener`
| `ekino.new_relic.interactor` | `Ekino\NewRelicBundle\NewRelic\NewRelicInteractorInterface`
| `ekino.new_relic.interactor.blackhole` | `Ekino\NewRelicBundle\NewRelic\BlackholeInteractor`
| `ekino.new_relic.interactor.logger` | `Ekino\NewRelicBundle\NewRelic\LoggingInteractorDecorator`
| `ekino.new_relic.interactor.real` | `Ekino\NewRelicBundle\NewRelic\NewRelicInteractor`
| `ekino.new_relic.request_listener` | `Ekino\NewRelicBundle\Listener\RequestListener`
| `ekino.new_relic.response_listener` | `Ekino\NewRelicBundle\Listener\ResponseListener`
| `ekino.new_relic.transaction_naming_strategy.controller` | `Ekino\NewRelicBundle\TransactionNamingStrategy\ControllerNamingStrategy`
| `ekino.new_relic.transaction_naming_strategy.route` | `Ekino\NewRelicBundle\TransactionNamingStrategy\RouteNamingStrategy`
| `ekino.new_relic.twig.new_relic_extension` | `Ekino\NewRelicBundle\Twig\NewRelicExtension`
| `ekino.new_relic` | `Ekino\NewRelicBundle\NewRelic\Config`
* The `NewRelicInteractorInterface` changed.
* added scalar type hinting on method declaration
* added method `addCustomEvent`
* added method `addCustomTracer`
* added method `excludeFromApdex`
* added method `recordDatastoreSegment`
* added method `setCaptureParams`
* added method `setUserAttributes`
* added method `stopTransactionTiming`
* rename method `noticeException` to `noticeThrowable`
* added parameter `$ignore` to the method `endTransaction`
* added parameters `$errno`, `$errstr`, `$errfile`, `$errline`, `$errcontext` to the method `noticeError`
* added parameter `$license` to the method `startTransaction`
* The `TransactionNamingStrategyInterface` changed.
* Added scalar type hinting on method declaration
* Allmost all classes changed to add scalar type hinting and protected methods have been removed, you should use composition over inheritance.
* The class `NewRelic\NewRelic` has been renamed to `NewRelic\Config`
================================================
FILE: composer.json
================================================
{
"name": "ekino/newrelic-bundle",
"type": "symfony-bundle",
"description": "Integrate New Relic into Symfony2",
"keywords": ["metric", "performance", "monitoring"],
"homepage": "https://github.com/ekino/EkinoNewRelicBundle",
"license": "MIT",
"authors": [
{
"name": "Thomas Rabaix",
"email": "thomas.rabaix@ekino.com",
"homepage": "http://ekino.com"
}
],
"require": {
"php": "^7.1 | ^8.0",
"symfony/config": "^3.4|^4.0|^5.0|^6.0",
"symfony/console": "^3.4|^4.0|^5.0|^6.0",
"symfony/dependency-injection": "^3.4|^4.0|^5.0|^6.0",
"symfony/event-dispatcher": "^3.4|^4.0|^5.0|^6.0",
"symfony/http-kernel": "^3.4|^4.0|^5.0|^6.0"
},
"require-dev": {
"matthiasnoback/symfony-dependency-injection-test": "^3.1|^4.0",
"symfony/framework-bundle": "^3.4|^4.0|^5.0|^6.0",
"symfony/phpunit-bridge": "^5.3",
"symfony/debug": ">3.4.21",
"twig/twig": "^1.32|^2.4",
"symfony/monolog-bundle": "^3.2"
},
"conflict": {
"twig/twig": "<1.32"
},
"suggest": {
"symfony/monolog-bundle": "^3.2"
},
"autoload": {
"psr-4": { "Ekino\\NewRelicBundle\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"autoload-dev": {
"psr-4": { "Ekino\\NewRelicBundle\\Tests\\": "Tests/" }
},
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
}
}
}
================================================
FILE: phpunit.xml.dist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
bootstrap="vendor/autoload.php"
>
<testsuites>
<testsuite name="Ekino New Relic Test Suite">
<directory>./Tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./Tests/</directory>
<directory>./Resources/</directory>
<directory>./vendor/</directory>
</exclude>
</whitelist>
</filter>
</phpunit>
gitextract_cqkhw3dv/ ├── .editorconfig ├── .gitattributes ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .php-cs-fixer.php ├── CHANGELOG.md ├── Command/ │ └── NotifyDeploymentCommand.php ├── DependencyInjection/ │ ├── Compiler/ │ │ └── MonologHandlerPass.php │ ├── Configuration.php │ └── EkinoNewRelicExtension.php ├── EkinoNewRelicBundle.php ├── Exception/ │ └── DeprecationException.php ├── LICENSE ├── Listener/ │ ├── CommandListener.php │ ├── DeprecationListener.php │ ├── ExceptionListener.php │ ├── RequestListener.php │ └── ResponseListener.php ├── Logging/ │ └── AdaptiveHandler.php ├── NewRelic/ │ ├── AdaptiveInteractor.php │ ├── BlackholeInteractor.php │ ├── Config.php │ ├── LoggingInteractorDecorator.php │ ├── NewRelicInteractor.php │ └── NewRelicInteractorInterface.php ├── README.md ├── Resources/ │ ├── config/ │ │ ├── command_listener.xml │ │ ├── deprecation_listener.xml │ │ ├── exception_listener.xml │ │ ├── http_listener.xml │ │ ├── monolog.xml │ │ ├── services.xml │ │ └── twig.xml │ └── recipes/ │ └── newrelic.rb ├── Tests/ │ ├── AppKernel.php │ ├── BundleInitializationTest.php │ ├── DependencyInjection/ │ │ ├── Compiler/ │ │ │ └── MonologHandlerPassTest.php │ │ ├── ConfigurationTest.php │ │ └── EkinoNewRelicExtensionTest.php │ ├── Listener/ │ │ ├── CommandListenerTest.php │ │ ├── DeprecationListenerTest.php │ │ ├── ExceptionListenerTest.php │ │ ├── RequestListenerTest.php │ │ └── ResponseListenerTest.php │ ├── NewRelic/ │ │ ├── ConfigTest.php │ │ └── LoggingInteractorDecoratorTest.php │ ├── TransactionNamingStrategy/ │ │ └── ControllerNamingStrategyTest.php │ └── Twig/ │ └── NewRelicExtensionTest.php ├── TransactionNamingStrategy/ │ ├── ControllerNamingStrategy.php │ ├── RouteNamingStrategy.php │ └── TransactionNamingStrategyInterface.php ├── Twig/ │ └── NewRelicExtension.php ├── UPGRADE-2.0.md ├── composer.json └── phpunit.xml.dist
SYMBOL INDEX (312 symbols across 36 files)
FILE: Command/NotifyDeploymentCommand.php
class NotifyDeploymentCommand (line 22) | class NotifyDeploymentCommand extends Command
method __construct (line 32) | public function __construct(Config $newrelic)
method configure (line 39) | protected function configure(): void
method execute (line 64) | protected function execute(InputInterface $input, OutputInterface $out...
method performRequest (line 102) | public function performRequest(string $api_key, string $payload, ?stri...
method createPayload (line 150) | private function createPayload(string $appName, InputInterface $input)...
FILE: DependencyInjection/Compiler/MonologHandlerPass.php
class MonologHandlerPass (line 21) | class MonologHandlerPass implements CompilerPassInterface
method process (line 23) | public function process(ContainerBuilder $container): void
method getChannels (line 56) | private function getChannels(ContainerBuilder $container)
FILE: DependencyInjection/Configuration.php
class Configuration (line 22) | class Configuration implements ConfigurationInterface
method getConfigTreeBuilder (line 24) | public function getConfigTreeBuilder(): TreeBuilder
FILE: DependencyInjection/EkinoNewRelicExtension.php
class EkinoNewRelicExtension (line 40) | class EkinoNewRelicExtension extends Extension
method load (line 42) | public function load(array $configs, ContainerBuilder $container): void
method getInteractorServiceId (line 135) | private function getInteractorServiceId(array $config): string
method getTransactionNamingServiceId (line 154) | private function getTransactionNamingServiceId(array $config): string
FILE: EkinoNewRelicBundle.php
class EkinoNewRelicBundle (line 21) | class EkinoNewRelicBundle extends Bundle
method build (line 23) | public function build(ContainerBuilder $container)
method boot (line 30) | public function boot()
method shutdown (line 39) | public function shutdown()
FILE: Exception/DeprecationException.php
class DeprecationException (line 19) | class DeprecationException extends \ErrorException
FILE: Listener/CommandListener.php
class CommandListener (line 23) | class CommandListener implements EventSubscriberInterface
method __construct (line 29) | public function __construct(Config $config, NewRelicInteractorInterfac...
method getSubscribedEvents (line 36) | public static function getSubscribedEvents(): array
method onConsoleCommand (line 44) | public function onConsoleCommand(ConsoleCommandEvent $event): void
method onConsoleError (line 85) | public function onConsoleError(ConsoleErrorEvent $event): void
FILE: Listener/DeprecationListener.php
class DeprecationListener (line 19) | class DeprecationListener
method __construct (line 24) | public function __construct(NewRelicInteractorInterface $interactor)
method register (line 29) | public function register(): void
method unregister (line 45) | public function unregister(): void
FILE: Listener/ExceptionListener.php
class ExceptionListener (line 26) | class ExceptionListener implements EventSubscriberInterface
method __construct (line 30) | public function __construct(NewRelicInteractorInterface $interactor)
method getSubscribedEvents (line 35) | public static function getSubscribedEvents(): array
method onKernelException (line 45) | public function onKernelException(KernelExceptionEvent $event): void
FILE: Listener/RequestListener.php
class RequestListener (line 25) | class RequestListener implements EventSubscriberInterface
method __construct (line 34) | public function __construct(
method getSubscribedEvents (line 50) | public static function getSubscribedEvents(): array
method setApplicationName (line 61) | public function setApplicationName(KernelRequestEvent $event): void
method setTransactionName (line 83) | public function setTransactionName(KernelRequestEvent $event): void
method setIgnoreTransaction (line 94) | public function setIgnoreTransaction(KernelRequestEvent $event): void
method isEventValid (line 113) | private function isEventValid(KernelRequestEvent $event): bool
FILE: Listener/ResponseListener.php
class ResponseListener (line 25) | class ResponseListener implements EventSubscriberInterface
method __construct (line 33) | public function __construct(
method getSubscribedEvents (line 47) | public static function getSubscribedEvents(): array
method onKernelResponse (line 56) | public function onKernelResponse(KernelResponseEvent $event): void
FILE: Logging/AdaptiveHandler.php
class AdaptiveHandler (line 19) | class AdaptiveHandler extends NewRelicHandler
method __construct (line 21) | public function __construct(
method write (line 31) | protected function write(array $record): void
FILE: NewRelic/AdaptiveInteractor.php
class AdaptiveInteractor (line 24) | class AdaptiveInteractor implements NewRelicInteractorInterface
method __construct (line 28) | public function __construct(NewRelicInteractorInterface $real, NewReli...
method setApplicationName (line 33) | public function setApplicationName(string $name, string $license = nul...
method setTransactionName (line 38) | public function setTransactionName(string $name): bool
method ignoreTransaction (line 43) | public function ignoreTransaction(): void
method addCustomEvent (line 48) | public function addCustomEvent(string $name, array $attributes): void
method addCustomMetric (line 53) | public function addCustomMetric(string $name, float $value): bool
method addCustomParameter (line 58) | public function addCustomParameter(string $name, $value): bool
method getBrowserTimingHeader (line 63) | public function getBrowserTimingHeader(bool $includeTags = true): string
method getBrowserTimingFooter (line 68) | public function getBrowserTimingFooter(bool $includeTags = true): string
method disableAutoRUM (line 73) | public function disableAutoRUM(): ?bool
method noticeThrowable (line 78) | public function noticeThrowable(\Throwable $e, string $message = null)...
method noticeError (line 83) | public function noticeError(
method enableBackgroundJob (line 93) | public function enableBackgroundJob(): void
method disableBackgroundJob (line 98) | public function disableBackgroundJob(): void
method startTransaction (line 103) | public function startTransaction(string $name = null, string $license ...
method endTransaction (line 108) | public function endTransaction(bool $ignore = false): bool
method excludeFromApdex (line 113) | public function excludeFromApdex(): void
method addCustomTracer (line 118) | public function addCustomTracer(string $name): bool
method setCaptureParams (line 123) | public function setCaptureParams(bool $enabled): void
method stopTransactionTiming (line 128) | public function stopTransactionTiming(): void
method recordDatastoreSegment (line 133) | public function recordDatastoreSegment(callable $func, array $parameters)
method setUserAttributes (line 138) | public function setUserAttributes(string $userValue, string $accountVa...
method getTraceMetadata (line 143) | public function getTraceMetadata(): array
method getLinkingMetadata (line 152) | public function getLinkingMetadata(): array
method isSampled (line 161) | public function isSampled(): bool
method insertDistributedTracingHeaders (line 170) | public function insertDistributedTracingHeaders(array &$headers): void
method acceptDistributedTraceHeaders (line 179) | public function acceptDistributedTraceHeaders(array $headers, string $...
FILE: NewRelic/BlackholeInteractor.php
class BlackholeInteractor (line 21) | class BlackholeInteractor implements NewRelicInteractorInterface
method setApplicationName (line 23) | public function setApplicationName(string $name, string $license = nul...
method setTransactionName (line 28) | public function setTransactionName(string $name): bool
method ignoreTransaction (line 33) | public function ignoreTransaction(): void
method addCustomEvent (line 37) | public function addCustomEvent(string $name, array $attributes): void
method addCustomMetric (line 41) | public function addCustomMetric(string $name, float $value): bool
method addCustomParameter (line 46) | public function addCustomParameter(string $name, $value): bool
method getBrowserTimingHeader (line 51) | public function getBrowserTimingHeader(bool $includeTags = true): string
method getBrowserTimingFooter (line 56) | public function getBrowserTimingFooter(bool $includeTags = true): string
method disableAutoRUM (line 61) | public function disableAutoRUM(): ?bool
method noticeThrowable (line 66) | public function noticeThrowable(\Throwable $e, string $message = null)...
method noticeError (line 70) | public function noticeError(
method enableBackgroundJob (line 79) | public function enableBackgroundJob(): void
method disableBackgroundJob (line 83) | public function disableBackgroundJob(): void
method startTransaction (line 87) | public function startTransaction(string $name = null, string $license ...
method endTransaction (line 92) | public function endTransaction(bool $ignore = false): bool
method excludeFromApdex (line 97) | public function excludeFromApdex(): void
method addCustomTracer (line 101) | public function addCustomTracer(string $name): bool
method setCaptureParams (line 106) | public function setCaptureParams(bool $enabled): void
method stopTransactionTiming (line 110) | public function stopTransactionTiming(): void
method recordDatastoreSegment (line 114) | public function recordDatastoreSegment(callable $func, array $parameters)
method setUserAttributes (line 119) | public function setUserAttributes(string $userValue, string $accountVa...
method getTraceMetadata (line 124) | public function getTraceMetadata(): array
method getLinkingMetadata (line 129) | public function getLinkingMetadata(): array
method isSampled (line 134) | public function isSampled(): bool
method insertDistributedTracingHeaders (line 139) | public function insertDistributedTracingHeaders(array &$headers): void
method acceptDistributedTraceHeaders (line 143) | public function acceptDistributedTraceHeaders(array $headers, string $...
FILE: NewRelic/Config.php
class Config (line 19) | class Config
method __construct (line 31) | public function __construct(?string $name, string $apiKey = null, stri...
method setCustomEvents (line 44) | public function setCustomEvents(array $customEvents): void
method getCustomEvents (line 49) | public function getCustomEvents(): array
method addCustomEvent (line 54) | public function addCustomEvent(string $name, array $attributes): void
method setCustomMetrics (line 59) | public function setCustomMetrics(array $customMetrics): void
method getCustomMetrics (line 64) | public function getCustomMetrics(): array
method setCustomParameters (line 69) | public function setCustomParameters(array $customParameters): void
method addCustomParameter (line 77) | public function addCustomParameter(string $name, $value): void
method addCustomMetric (line 82) | public function addCustomMetric(string $name, float $value): void
method getCustomParameters (line 87) | public function getCustomParameters(): array
method getName (line 92) | public function getName(): string
method getDeploymentNames (line 100) | public function getDeploymentNames(): array
method getApiKey (line 105) | public function getApiKey(): ?string
method getApiHost (line 110) | public function getApiHost(): ?string
method getLicenseKey (line 115) | public function getLicenseKey(): ?string
method getXmit (line 120) | public function getXmit(): bool
FILE: NewRelic/LoggingInteractorDecorator.php
class LoggingInteractorDecorator (line 19) | class LoggingInteractorDecorator implements NewRelicInteractorInterface
method __construct (line 24) | public function __construct(NewRelicInteractorInterface $interactor, L...
method setApplicationName (line 30) | public function setApplicationName(string $name, string $license = nul...
method setTransactionName (line 37) | public function setTransactionName(string $name): bool
method ignoreTransaction (line 44) | public function ignoreTransaction(): void
method addCustomEvent (line 50) | public function addCustomEvent(string $name, array $attributes): void
method addCustomMetric (line 56) | public function addCustomMetric(string $name, float $value): bool
method addCustomParameter (line 63) | public function addCustomParameter(string $name, $value): bool
method getBrowserTimingHeader (line 70) | public function getBrowserTimingHeader(bool $includeTags = true): string
method getBrowserTimingFooter (line 77) | public function getBrowserTimingFooter(bool $includeTags = true): string
method disableAutoRUM (line 84) | public function disableAutoRUM(): ?bool
method noticeError (line 91) | public function noticeError(
method noticeThrowable (line 108) | public function noticeThrowable(\Throwable $e, string $message = null)...
method enableBackgroundJob (line 117) | public function enableBackgroundJob(): void
method disableBackgroundJob (line 123) | public function disableBackgroundJob(): void
method endTransaction (line 129) | public function endTransaction(bool $ignore = false): bool
method startTransaction (line 136) | public function startTransaction(string $name = null, string $license ...
method excludeFromApdex (line 143) | public function excludeFromApdex(): void
method addCustomTracer (line 149) | public function addCustomTracer(string $name): bool
method setCaptureParams (line 156) | public function setCaptureParams(bool $enabled): void
method stopTransactionTiming (line 162) | public function stopTransactionTiming(): void
method recordDatastoreSegment (line 168) | public function recordDatastoreSegment(callable $func, array $parameters)
method setUserAttributes (line 177) | public function setUserAttributes(string $userValue, string $accountVa...
method getTraceMetadata (line 188) | public function getTraceMetadata(): array
method getLinkingMetadata (line 197) | public function getLinkingMetadata(): array
method isSampled (line 206) | public function isSampled(): bool
method insertDistributedTracingHeaders (line 215) | public function insertDistributedTracingHeaders(array &$headers): void
method acceptDistributedTraceHeaders (line 222) | public function acceptDistributedTraceHeaders(array $headers, string $...
FILE: NewRelic/NewRelicInteractor.php
class NewRelicInteractor (line 16) | class NewRelicInteractor implements NewRelicInteractorInterface
method setApplicationName (line 18) | public function setApplicationName(string $name, string $license = nul...
method setTransactionName (line 23) | public function setTransactionName(string $name): bool
method ignoreTransaction (line 28) | public function ignoreTransaction(): void
method addCustomEvent (line 33) | public function addCustomEvent(string $name, array $attributes): void
method addCustomMetric (line 38) | public function addCustomMetric(string $name, float $value): bool
method addCustomParameter (line 43) | public function addCustomParameter(string $name, $value): bool
method getBrowserTimingHeader (line 48) | public function getBrowserTimingHeader(bool $includeTags = true): string
method getBrowserTimingFooter (line 53) | public function getBrowserTimingFooter(bool $includeTags = true): string
method disableAutoRUM (line 58) | public function disableAutoRUM(): ?bool
method noticeError (line 63) | public function noticeError(int $errno, string $errstr, string $errfil...
method noticeThrowable (line 68) | public function noticeThrowable(\Throwable $e, string $message = null)...
method enableBackgroundJob (line 73) | public function enableBackgroundJob(): void
method disableBackgroundJob (line 78) | public function disableBackgroundJob(): void
method endTransaction (line 83) | public function endTransaction(bool $ignore = false): bool
method startTransaction (line 88) | public function startTransaction(string $name = null, string $license ...
method excludeFromApdex (line 101) | public function excludeFromApdex(): void
method addCustomTracer (line 106) | public function addCustomTracer(string $name): bool
method setCaptureParams (line 111) | public function setCaptureParams(bool $enabled): void
method stopTransactionTiming (line 116) | public function stopTransactionTiming(): void
method recordDatastoreSegment (line 121) | public function recordDatastoreSegment(callable $func, array $parameters)
method setUserAttributes (line 126) | public function setUserAttributes(string $userValue, string $accountVa...
method getTraceMetadata (line 131) | public function getTraceMetadata(): array
method getLinkingMetadata (line 140) | public function getLinkingMetadata(): array
method isSampled (line 149) | public function isSampled(): bool
method insertDistributedTracingHeaders (line 158) | public function insertDistributedTracingHeaders(array &$headers): void
method acceptDistributedTraceHeaders (line 167) | public function acceptDistributedTraceHeaders(array $headers, string $...
FILE: NewRelic/NewRelicInteractorInterface.php
type NewRelicInteractorInterface (line 25) | interface NewRelicInteractorInterface
method setApplicationName (line 32) | public function setApplicationName(string $name, string $license = nul...
method setTransactionName (line 39) | public function setTransactionName(string $name): bool;
method ignoreTransaction (line 46) | public function ignoreTransaction(): void;
method addCustomEvent (line 53) | public function addCustomEvent(string $name, array $attributes): void;
method addCustomMetric (line 60) | public function addCustomMetric(string $name, float $value): bool;
method addCustomParameter (line 67) | public function addCustomParameter(string $name, $value): bool;
method getBrowserTimingHeader (line 74) | public function getBrowserTimingHeader(bool $includeTags = true): string;
method getBrowserTimingFooter (line 81) | public function getBrowserTimingFooter(bool $includeTags = true): string;
method disableAutoRUM (line 88) | public function disableAutoRUM(): ?bool;
method noticeThrowable (line 96) | public function noticeThrowable(\Throwable $e, string $message = null)...
method noticeError (line 101) | public function noticeError(int $errno, string $errstr, string $errfil...
method enableBackgroundJob (line 106) | public function enableBackgroundJob(): void;
method disableBackgroundJob (line 111) | public function disableBackgroundJob(): void;
method startTransaction (line 118) | public function startTransaction(string $name = null, string $license ...
method endTransaction (line 125) | public function endTransaction(bool $ignore = false): bool;
method excludeFromApdex (line 132) | public function excludeFromApdex(): void;
method addCustomTracer (line 140) | public function addCustomTracer(string $name): bool;
method setCaptureParams (line 147) | public function setCaptureParams(bool $enabled): void;
method stopTransactionTiming (line 154) | public function stopTransactionTiming(): void;
method recordDatastoreSegment (line 163) | public function recordDatastoreSegment(callable $func, array $paramete...
method setUserAttributes (line 170) | public function setUserAttributes(string $userValue, string $accountVa...
FILE: Tests/AppKernel.php
class AppKernel (line 24) | class AppKernel extends Kernel
method __construct (line 39) | public function __construct($cachePrefix)
method getCacheDir (line 45) | public function getCacheDir(): string
method getLogDir (line 50) | public function getLogDir(): string
method getProjectDir (line 55) | public function getProjectDir(): string
method setRootDir (line 67) | public function setRootDir($rootDir)
method setProjectDir (line 75) | public function setProjectDir($projectDir)
method registerBundles (line 80) | public function registerBundles(): iterable
method registerContainerConfiguration (line 92) | public function registerContainerConfiguration(LoaderInterface $loader)
method loadRoutes (line 130) | public function loadRoutes(LoaderInterface $loader)
method buildContainer (line 138) | protected function buildContainer(): ContainerBuilder
FILE: Tests/BundleInitializationTest.php
class BundleInitializationTest (line 28) | class BundleInitializationTest extends TestCase
method getBundleClass (line 30) | protected function getBundleClass()
method testInitBundle (line 35) | public function testInitBundle()
FILE: Tests/DependencyInjection/Compiler/MonologHandlerPassTest.php
class MonologHandlerPassTest (line 21) | class MonologHandlerPassTest extends AbstractCompilerPassTestCase
method registerCompilerPass (line 23) | protected function registerCompilerPass(ContainerBuilder $container): ...
method testProcessChannel (line 28) | public function testProcessChannel()
method testProcessChannelAllChannels (line 43) | public function testProcessChannelAllChannels()
method testProcessChannelExcludeChannels (line 58) | public function testProcessChannelExcludeChannels()
FILE: Tests/DependencyInjection/ConfigurationTest.php
class ConfigurationTest (line 21) | class ConfigurationTest extends TestCase
method testIgnoredRoutes (line 23) | public function testIgnoredRoutes()
method testIgnoredPaths (line 42) | public function testIgnoredPaths()
method testIgnoredCommands (line 61) | public function testIgnoredCommands()
method testDefaults (line 80) | public function testDefaults()
method ignoredRoutesProvider (line 96) | public static function ignoredRoutesProvider()
method ignoredPathsProvider (line 105) | public static function ignoredPathsProvider()
method ignoredCommandsProvider (line 114) | public static function ignoredCommandsProvider()
method deploymentNamesProvider (line 123) | public static function deploymentNamesProvider()
method testDeploymentNames (line 135) | public function testDeploymentNames($deploymentNameConfig, $expected)
method testIgnoreRoutes (line 149) | public function testIgnoreRoutes($ignoredRoutesConfig, $expected)
method testIgnorePaths (line 161) | public function testIgnorePaths($ignoredPathsConfig, $expected)
method testIgnoreCommands (line 173) | public function testIgnoreCommands($ignoredCommandsConfig, $expected)
FILE: Tests/DependencyInjection/EkinoNewRelicExtensionTest.php
class EkinoNewRelicExtensionTest (line 27) | class EkinoNewRelicExtensionTest extends AbstractExtensionTestCase
method getContainerExtensions (line 29) | protected function getContainerExtensions(): array
method setUp (line 34) | protected function setUp(): void
method testDefaultConfiguration (line 41) | public function testDefaultConfiguration()
method testAlternativeConfiguration (line 50) | public function testAlternativeConfiguration()
method testDeprecation (line 63) | public function testDeprecation()
method testMonolog (line 70) | public function testMonolog()
method testMonologDisabled (line 79) | public function testMonologDisabled()
method testConfigDisabled (line 89) | public function testConfigDisabled()
method testConfigDisabledWithInteractor (line 98) | public function testConfigDisabledWithInteractor()
method testConfigEnabledWithInteractor (line 108) | public function testConfigEnabledWithInteractor()
FILE: Tests/Listener/CommandListenerTest.php
class CommandListenerTest (line 29) | class CommandListenerTest extends TestCase
method testCommandMarkedAsBackgroundJob (line 31) | public function testCommandMarkedAsBackgroundJob()
method testIgnoreBackgroundJob (line 71) | public function testIgnoreBackgroundJob()
method testConsoleError (line 87) | public function testConsoleError()
method testConsoleErrorsWithThrowable (line 106) | public function testConsoleErrorsWithThrowable()
FILE: Tests/Listener/DeprecationListenerTest.php
class DeprecationListenerTest (line 21) | class DeprecationListenerTest extends TestCase
method testDeprecationIsReported (line 23) | public function testDeprecationIsReported()
method testDeprecationIsReportedRegardlessErrorReporting (line 42) | public function testDeprecationIsReportedRegardlessErrorReporting()
method testOtherErrorAreIgnored (line 61) | public function testOtherErrorAreIgnored()
method testInitialHandlerIsCalled (line 78) | public function testInitialHandlerIsCalled()
method testUnregisterRemovesHandler (line 98) | public function testUnregisterRemovesHandler()
method testUnregisterRestorePreviousHandler (line 115) | public function testUnregisterRestorePreviousHandler()
class DummyHandler (line 135) | class DummyHandler
method __invoke (line 137) | public function __invoke()
FILE: Tests/Listener/ExceptionListenerTest.php
class ExceptionListenerTest (line 25) | class ExceptionListenerTest extends TestCase
method testOnKernelException (line 27) | public function testOnKernelException()
method testOnKernelExceptionWithHttp (line 44) | public function testOnKernelExceptionWithHttp()
FILE: Tests/Listener/RequestListenerTest.php
class RequestListenerTest (line 27) | class RequestListenerTest extends TestCase
method testSubRequest (line 29) | public function testSubRequest()
method testMasterRequest (line 45) | public function testMasterRequest()
method testPathIsIgnored (line 64) | public function testPathIsIgnored()
method testRouteIsIgnored (line 81) | public function testRouteIsIgnored()
method testSymfonyCacheEnabled (line 98) | public function testSymfonyCacheEnabled()
method testSymfonyCacheDisabled (line 114) | public function testSymfonyCacheDisabled()
FILE: Tests/Listener/ResponseListenerTest.php
class ResponseListenerTest (line 27) | class ResponseListenerTest extends TestCase
method setUp (line 29) | protected function setUp(): void
method testOnKernelResponseOnlyMasterRequestsAreProcessed (line 42) | public function testOnKernelResponseOnlyMasterRequestsAreProcessed()
method testOnKernelResponseWithOnlyCustomMetricsAndParameters (line 52) | public function testOnKernelResponseWithOnlyCustomMetricsAndParameters()
method testOnKernelResponseInstrumentDisabledInRequest (line 106) | public function testOnKernelResponseInstrumentDisabledInRequest()
method testSymfonyCacheEnabled (line 118) | public function testSymfonyCacheEnabled()
method testSymfonyCacheDisabled (line 130) | public function testSymfonyCacheDisabled()
method testOnKernelResponseOnlyInstrumentHTMLResponses (line 145) | public function testOnKernelResponseOnlyInstrumentHTMLResponses($conte...
method providerOnKernelResponseOnlyInstrumentHTMLResponses (line 160) | public function providerOnKernelResponseOnlyInstrumentHTMLResponses()
method testInteractionWithTwigExtensionHeader (line 182) | public function testInteractionWithTwigExtensionHeader()
method testInteractionWithTwigExtensionFooter (line 204) | public function testInteractionWithTwigExtensionFooter()
method testInteractionWithTwigExtensionHeaderFooter (line 226) | public function testInteractionWithTwigExtensionHeaderFooter()
method setUpNoCustomMetricsOrParameters (line 248) | private function setUpNoCustomMetricsOrParameters()
method createRequestMock (line 259) | private function createRequestMock($instrumentEnabled = true)
method createResponseMock (line 271) | private function createResponseMock($content = null, $expectsSetConten...
method createFilterResponseEventDummy (line 290) | private function createFilterResponseEventDummy(Request $request = nul...
FILE: Tests/NewRelic/ConfigTest.php
class ConfigTest (line 19) | class ConfigTest extends TestCase
method testGeneric (line 21) | public function testGeneric()
method testDefaults (line 70) | public function testDefaults()
FILE: Tests/NewRelic/LoggingInteractorDecoratorTest.php
class LoggingInteractorDecoratorTest (line 21) | class LoggingInteractorDecoratorTest extends TestCase
method testGeneric (line 26) | public function testGeneric(string $method, array $arguments, $return)
method provideMethods (line 44) | public function provideMethods()
method getTypeStub (line 65) | private function getTypeStub(?\ReflectionType $type)
FILE: Tests/TransactionNamingStrategy/ControllerNamingStrategyTest.php
class ControllerNamingStrategyTest (line 20) | class ControllerNamingStrategyTest extends TestCase
method testControllerAsString (line 22) | public function testControllerAsString()
method testControllerAsClosure (line 31) | public function testControllerAsClosure()
method testControllerAsCallback (line 41) | public function testControllerAsCallback()
method testControllerUnknown (line 50) | public function testControllerUnknown()
FILE: Tests/Twig/NewRelicExtensionTest.php
class NewRelicExtensionTest (line 21) | class NewRelicExtensionTest extends TestCase
method setUp (line 33) | protected function setUp(): void
method testInitialSetup (line 45) | public function testInitialSetup()
method testHeaderException (line 57) | public function testHeaderException()
method testFooterException (line 78) | public function testFooterException()
method testPreparingOfInteractor (line 99) | public function testPreparingOfInteractor()
FILE: TransactionNamingStrategy/ControllerNamingStrategy.php
class ControllerNamingStrategy (line 22) | class ControllerNamingStrategy implements TransactionNamingStrategyInter...
method getTransactionName (line 24) | public function getTransactionName(Request $request): string
FILE: TransactionNamingStrategy/RouteNamingStrategy.php
class RouteNamingStrategy (line 21) | class RouteNamingStrategy implements TransactionNamingStrategyInterface
method getTransactionName (line 23) | public function getTransactionName(Request $request): string
FILE: TransactionNamingStrategy/TransactionNamingStrategyInterface.php
type TransactionNamingStrategyInterface (line 18) | interface TransactionNamingStrategyInterface
method getTransactionName (line 20) | public function getTransactionName(Request $request): string;
FILE: Twig/NewRelicExtension.php
class NewRelicExtension (line 24) | class NewRelicExtension extends AbstractExtension
method __construct (line 32) | public function __construct(
method getFunctions (line 42) | public function getFunctions(): array
method getNewrelicBrowserTimingHeader (line 53) | public function getNewrelicBrowserTimingHeader(): string
method getNewrelicBrowserTimingFooter (line 69) | public function getNewrelicBrowserTimingFooter(): string
method isHeaderCalled (line 84) | public function isHeaderCalled(): bool
method isFooterCalled (line 89) | public function isFooterCalled(): bool
method isUsed (line 94) | public function isUsed(): bool
method prepareInteractor (line 99) | private function prepareInteractor(): void
Condensed preview — 55 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (183K chars).
[
{
"path": ".editorconfig",
"chars": 147,
"preview": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 4\nindent_style = space\ninsert_final_newline = true\ntrim_"
},
{
"path": ".gitattributes",
"chars": 288,
"preview": ".editorconfig export-ignore\n.gitattributes export-ignore\n.gitignore export-ignore\n/.php_cs export"
},
{
"path": ".github/workflows/ci.yml",
"chars": 2223,
"preview": "name: CI\n\non:\n pull_request: ~\n push:\n branches:\n - master\n\njobs:\n php-cs-fixer:\n name: PHP-CS-Fixer\n r"
},
{
"path": ".gitignore",
"chars": 62,
"preview": ".php_cs.cache\nbuild\nphpunit.xml\ncoverage\ncomposer.lock\nvendor\n"
},
{
"path": ".php-cs-fixer.php",
"chars": 1285,
"preview": "<?php\n\n$header = <<<'EOF'\nThis file is part of Ekino New Relic bundle.\n\n(c) Ekino - Thomas Rabaix <thomas.rabaix@ekino.c"
},
{
"path": "CHANGELOG.md",
"chars": 3007,
"preview": "# Changelog\n\n## v2.4.0\n\n### Added\n\n- Support for distributed tracing functions provided by the NewRelic PHP extension ar"
},
{
"path": "Command/NotifyDeploymentCommand.php",
"chars": 5761,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "DependencyInjection/Compiler/MonologHandlerPass.php",
"chars": 2893,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "DependencyInjection/Configuration.php",
"chars": 8479,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "DependencyInjection/EkinoNewRelicExtension.php",
"chars": 7001,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "EkinoNewRelicBundle.php",
"chars": 1199,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "Exception/DeprecationException.php",
"chars": 429,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "LICENSE",
"chars": 1091,
"preview": "The MIT License\n\nCopyright (c) 2012 Ekino - thomas.rabaix@ekino.com\n\nPermission is hereby granted, free of charge, to an"
},
{
"path": "Listener/CommandListener.php",
"chars": 3015,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "Listener/DeprecationListener.php",
"chars": 1408,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "Listener/ExceptionListener.php",
"chars": 1829,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "Listener/RequestListener.php",
"chars": 3830,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "Listener/ResponseListener.php",
"chars": 4391,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "Logging/AdaptiveHandler.php",
"chars": 912,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "NewRelic/AdaptiveInteractor.php",
"chars": 5760,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "NewRelic/BlackholeInteractor.php",
"chars": 2967,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "NewRelic/Config.php",
"chars": 2995,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "NewRelic/LoggingInteractorDecorator.php",
"chars": 7397,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "NewRelic/NewRelicInteractor.php",
"chars": 5158,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "NewRelic/NewRelicInteractorInterface.php",
"chars": 6412,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "README.md",
"chars": 11933,
"preview": "Ekino NewRelic Bundle\n=====================\n\n["
},
{
"path": "Tests/AppKernel.php",
"chars": 4299,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "Tests/BundleInitializationTest.php",
"chars": 1591,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "Tests/DependencyInjection/Compiler/MonologHandlerPassTest.php",
"chars": 3830,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "Tests/DependencyInjection/ConfigurationTest.php",
"chars": 7281,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "Tests/DependencyInjection/EkinoNewRelicExtensionTest.php",
"chars": 3613,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "Tests/Listener/CommandListenerTest.php",
"chars": 4749,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "Tests/Listener/DeprecationListenerTest.php",
"chars": 4411,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "Tests/Listener/ExceptionListenerTest.php",
"chars": 2266,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "Tests/Listener/RequestListenerTest.php",
"chars": 5959,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "Tests/Listener/ResponseListenerTest.php",
"chars": 12603,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "Tests/NewRelic/ConfigTest.php",
"chars": 2338,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "Tests/NewRelic/LoggingInteractorDecoratorTest.php",
"chars": 2830,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "Tests/TransactionNamingStrategy/ControllerNamingStrategyTest.php",
"chars": 1861,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "Tests/Twig/NewRelicExtensionTest.php",
"chars": 4328,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "TransactionNamingStrategy/ControllerNamingStrategy.php",
"chars": 1480,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "TransactionNamingStrategy/RouteNamingStrategy.php",
"chars": 640,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "TransactionNamingStrategy/TransactionNamingStrategyInterface.php",
"chars": 498,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "Twig/NewRelicExtension.php",
"chars": 3114,
"preview": "<?php\n\ndeclare(strict_types=1);\n\n/*\n * This file is part of Ekino New Relic bundle.\n *\n * (c) Ekino - Thomas Rabaix <tho"
},
{
"path": "UPGRADE-2.0.md",
"chars": 5130,
"preview": "UPGRADE FROM 1.x to 2.0\n=======================\n\n> Many internal things in the bundle has changed. If you only installed"
},
{
"path": "composer.json",
"chars": 1533,
"preview": "{\n \"name\": \"ekino/newrelic-bundle\",\n \"type\": \"symfony-bundle\",\n \"description\": \"Integrate New Relic into Symfon"
},
{
"path": "phpunit.xml.dist",
"chars": 837,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<phpunit backupGlobals=\"false\"\n backupStaticAttributes=\"false\"\n "
}
]
About this extraction
This page contains the full source code of the ekino/EkinoNewRelicBundle GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 55 files (168.9 KB), approximately 41.0k tokens, and a symbol index with 312 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.