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 ================================================ 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 ================================================ * * 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('No deployment application configured.'); 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("Deployment not recorded to '%s': API key invalid", $appName)); $exitCode = self::EXIT_UNAUTHORIZED; break; case null: $output->writeLn(sprintf("Deployment not recorded to '%s': Did not understand response", $appName)); $exitCode = self::EXIT_HTTP_ERROR; break; default: $output->writeLn(sprintf("Deployment not recorded to '%s': Received HTTP status %d", $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>/', $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 ================================================ * * 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 ================================================ * * 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 ================================================ * * 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 ================================================ * * 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 ================================================ * * 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 ================================================ * * 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 ================================================ * * 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 ================================================ * * 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 ================================================ * * 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 ================================================ * * 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('||i', '$0'.$this->interactor->getBrowserTimingHeader(), $responseContent); } if (null === $this->newRelicTwigExtension || false === $this->newRelicTwigExtension->isFooterCalled()) { $responseContent = preg_replace('||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 ================================================ * * 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 ================================================ * * 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 */ 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 ================================================ * * 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 ================================================ * * 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 ================================================ * * 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 ================================================ * * 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 ================================================ * * 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 ===================== [![Build Status](https://secure.travis-ci.org/ekino/EkinoNewRelicBundle.png?branch=master)](http://travis-ci.org/ekino/EkinoNewRelicBundle) [![Latest Version](https://img.shields.io/github/release/ekino/EkinoNewRelicBundle.svg?style=flat-square)](https://github.com/ekino/EkinoNewRelicBundle/releases) [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/ekino/EkinoNewRelicBundle.svg?style=flat-square)](https://scrutinizer-ci.com/g/ekino/EkinoNewRelicBundle) [![Quality Score](https://img.shields.io/scrutinizer/g/ekino/EkinoNewRelicBundle.svg?style=flat-square)](https://scrutinizer-ci.com/g/ekino/EkinoNewRelicBundle) [![Total Downloads](https://img.shields.io/packagist/dt/ekino/newrelic-bundle.svg?style=flat-square)](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. ![image](https://cloud.githubusercontent.com/assets/670655/5153003/5c956c1e-7235-11e4-9eb2-d203fa42420b.png) 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. ![Ekino NewRelicBundle](https://dl.dropbox.com/s/bufb6f8o0end5xo/ekino_newrelic_bundle.png "Ekino NewRelicBundle") ## 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 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 ================================================ ================================================ FILE: Resources/config/deprecation_listener.xml ================================================ ================================================ FILE: Resources/config/exception_listener.xml ================================================ ================================================ FILE: Resources/config/http_listener.xml ================================================ ================================================ FILE: Resources/config/monolog.xml ================================================ ================================================ FILE: Resources/config/services.xml ================================================ ================================================ FILE: Resources/config/twig.xml ================================================ ================================================ 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 ================================================ * * 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 ================================================ * * 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 */ 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 ================================================ * * 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 ================================================ * * 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 ================================================ * * 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 ================================================ * * 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 ================================================ * * 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 ================================================ * * 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 ================================================ * * 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 ================================================ * * 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'], ['
head
', '
head
', 'text/html'], ['
content
', '
content
', 'text/html'], // head, body tags ['</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>