diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b57968c..62fe3a96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,27 @@ ### Added + - New `\Scoutapm\Config\ConfigKey` class containing `public const`s for configuration key names (#83) + - Added config key `log_level` which overrides Scout APM's minimum log level (#83) + +### Fixed + +### Changed + + - **[BC]** Renamed the following configuration keys (#83) + - `log_level` => `core_agent_log_level` + - `log_file` => `core_agent_log_file` + - `config_file` => `core_agent_config_file` + - `socket_path` => `core_agent_socket_path` + - `download_url` => `core_agent_download_url` + - Improved stack trace filtering (#61) + +## [0.2.2] 2019-09-26 + ### Fixed + - Corrected naming of core agent config values (#80) + ## [0.2.1] 2019-09-25 ### Changed @@ -16,8 +35,8 @@ ### Changed -- Internal data model now perserves order (#47) -- Loosen several depencency version requirements (#50) +- Internal data model now preserves order (#47) +- Loosen several dependency version requirements (#50) - Licensed as MIT - Initial support for Scout Native Extension (#42, #54) diff --git a/README.md b/README.md index 17090b98..37d1ba70 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,12 @@ To install the ScoutAPM Agent for a specific framework, use the specific package ```php use Scoutapm\Agent; use Scoutapm\Config; +use Scoutapm\Config\ConfigKey; $agent = Agent::fromConfig(Config::fromArray([ - 'name' => 'Your application name', - 'key' => 'your scout key', - 'monitor' => true, + ConfigKey::APPLICATION_NAME => 'Your application name', + ConfigKey::APPLICATION_KEY => 'your scout key', + ConfigKey::MONITORING_ENABLED => true, ])); // If the core agent is not already running, this will download and run it (from /tmp by default) $agent->connect(); diff --git a/src/Agent.php b/src/Agent.php index 7e5bb073..b429c81e 100644 --- a/src/Agent.php +++ b/src/Agent.php @@ -9,6 +9,7 @@ use DateTimeZone; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; +use Scoutapm\Config\ConfigKey; use Scoutapm\Config\IgnoredEndpoints; use Scoutapm\Connector\Connector; use Scoutapm\Connector\Exception\FailedToConnect; @@ -24,6 +25,7 @@ use Scoutapm\Events\Span\Span; use Scoutapm\Extension\ExtentionCapabilities; use Scoutapm\Extension\PotentiallyAvailableExtensionCapabilities; +use Scoutapm\Logger\FilteredLogLevelDecorator; final class Agent implements ScoutApmAgent { @@ -63,14 +65,21 @@ public function __construct(Config $configuration, Connector $connector, LoggerI $this->logger = $logger; $this->phpExtension = $phpExtension; + if (! $this->logger instanceof FilteredLogLevelDecorator) { + $this->logger = new FilteredLogLevelDecorator( + $this->logger, + $this->config->get(ConfigKey::LOG_LEVEL) + ); + } + $this->request = new Request(); - $this->ignoredEndpoints = new IgnoredEndpoints($configuration->get('ignore')); + $this->ignoredEndpoints = new IgnoredEndpoints($configuration->get(ConfigKey::IGNORED_ENDPOINTS)); } private static function createConnectorFromConfig(Config $config) : SocketConnector { - return new SocketConnector($config->get('socket_path')); + return new SocketConnector($config->get(ConfigKey::CORE_AGENT_SOCKET_PATH)); } /** @@ -108,10 +117,10 @@ public function connect() : void $this->config, $this->logger, new Downloader( - $this->config->get('core_agent_dir') . '/' . $this->config->get('core_agent_full_name'), - $this->config->get('core_agent_full_name'), + $this->config->get(ConfigKey::CORE_AGENT_DIRECTORY) . '/' . $this->config->get(ConfigKey::CORE_AGENT_FULL_NAME), + $this->config->get(ConfigKey::CORE_AGENT_FULL_NAME), $this->logger, - $this->config->get('download_url') + $this->config->get(ConfigKey::CORE_AGENT_DOWNLOAD_URL) ) ); $manager->launch(); @@ -135,7 +144,7 @@ public function connect() : void /** {@inheritDoc} */ public function enabled() : bool { - return $this->config->get('monitor'); + return $this->config->get(ConfigKey::MONITORING_ENABLED); } /** {@inheritDoc} */ @@ -258,9 +267,9 @@ public function send() : bool try { if (! $this->connector->sendCommand(new RegisterMessage( - (string) $this->config->get('name'), - (string) $this->config->get('key'), - $this->config->get('api_version') + (string) $this->config->get(ConfigKey::APPLICATION_NAME), + (string) $this->config->get(ConfigKey::APPLICATION_KEY), + $this->config->get(ConfigKey::API_VERSION) ))) { return false; } diff --git a/src/Config.php b/src/Config.php index e2411d07..7dded80f 100644 --- a/src/Config.php +++ b/src/Config.php @@ -8,6 +8,7 @@ namespace Scoutapm; +use Scoutapm\Config\ConfigKey; use Scoutapm\Config\Source\DefaultSource; use Scoutapm\Config\Source\DerivedSource; use Scoutapm\Config\Source\EnvSource; @@ -43,8 +44,8 @@ public function __construct() ]; $this->coercions = [ - 'monitor' => new CoerceBoolean(), - 'ignore' => new CoerceJson(), + ConfigKey::MONITORING_ENABLED => new CoerceBoolean(), + ConfigKey::IGNORED_ENDPOINTS => new CoerceJson(), ]; } diff --git a/src/Config/ConfigKey.php b/src/Config/ConfigKey.php new file mode 100644 index 00000000..9fa929dd --- /dev/null +++ b/src/Config/ConfigKey.php @@ -0,0 +1,26 @@ + '1.0', - 'core_agent_dir' => '/tmp/scout_apm_core', - 'core_agent_download' => true, - 'core_agent_launch' => true, - 'core_agent_version' => 'v1.2.2', - 'download_url' => 'https://s3-us-west-1.amazonaws.com/scout-public-downloads/apm_core_agent/release', - 'monitor' => false, - 'ignore' => [], + ConfigKey::API_VERSION => '1.0', + ConfigKey::CORE_AGENT_DIRECTORY => '/tmp/scout_apm_core', + ConfigKey::CORE_AGENT_DOWNLOAD_ENABLED => true, + ConfigKey::CORE_AGENT_LAUNCH_ENABLED => true, + ConfigKey::CORE_AGENT_VERSION => 'v1.2.2', + ConfigKey::CORE_AGENT_DOWNLOAD_URL => 'https://s3-us-west-1.amazonaws.com/scout-public-downloads/apm_core_agent/release', + ConfigKey::MONITORING_ENABLED => false, + ConfigKey::IGNORED_ENDPOINTS => [], + ConfigKey::LOG_LEVEL => 'debug', ]; } } diff --git a/src/Config/Source/DerivedSource.php b/src/Config/Source/DerivedSource.php index a66c633f..77268bc9 100644 --- a/src/Config/Source/DerivedSource.php +++ b/src/Config/Source/DerivedSource.php @@ -13,6 +13,7 @@ namespace Scoutapm\Config\Source; use Scoutapm\Config; +use Scoutapm\Config\ConfigKey; use function php_uname; /** @internal */ @@ -44,11 +45,11 @@ public function hasKey(string $key) : bool public function get(string $key) { switch ($key) { - case 'socket_path': + case ConfigKey::CORE_AGENT_SOCKET_PATH: return $this->socketPath(); - case 'core_agent_full_name': + case ConfigKey::CORE_AGENT_FULL_NAME: return $this->coreAgentFullName(); - case 'core_agent_triple': + case ConfigKey::CORE_AGENT_TRIPLE: return $this->coreAgentTriple(); case 'testing': return $this->testing(); @@ -59,8 +60,8 @@ public function get(string $key) private function socketPath() : string { - $dir = $this->config->get('core_agent_dir'); - $fullName = $this->config->get('core_agent_full_name'); + $dir = $this->config->get(ConfigKey::CORE_AGENT_DIRECTORY); + $fullName = $this->config->get(ConfigKey::CORE_AGENT_FULL_NAME); return $dir . '/' . $fullName . '/scout-agent.sock'; } @@ -68,8 +69,8 @@ private function socketPath() : string private function coreAgentFullName() : string { $name = 'scout_apm_core'; - $version = $this->config->get('core_agent_version'); - $triple = $this->config->get('core_agent_triple'); + $version = $this->config->get(ConfigKey::CORE_AGENT_VERSION); + $triple = $this->config->get(ConfigKey::CORE_AGENT_TRIPLE); return $name . '-' . $version . '-' . $triple; } @@ -100,7 +101,7 @@ private function coreAgentTriple() : string */ private function testing() : string { - $version = $this->config->get('api_version'); + $version = $this->config->get(ConfigKey::API_VERSION); return 'derived api version: ' . $version; } diff --git a/src/CoreAgent/AutomaticDownloadAndLaunchManager.php b/src/CoreAgent/AutomaticDownloadAndLaunchManager.php index 8440f845..baff8ee3 100644 --- a/src/CoreAgent/AutomaticDownloadAndLaunchManager.php +++ b/src/CoreAgent/AutomaticDownloadAndLaunchManager.php @@ -6,12 +6,14 @@ use Psr\Log\LoggerInterface; use Scoutapm\Config; +use Scoutapm\Config\ConfigKey; use Throwable; use function array_map; use function exec; use function file_get_contents; use function hash; use function implode; +use function sprintf; /** @internal */ final class AutomaticDownloadAndLaunchManager implements Manager @@ -35,24 +37,28 @@ public function __construct(Config $config, LoggerInterface $logger, Downloader { $this->config = $config; $this->logger = $logger; - $this->coreAgentDir = $config->get('core_agent_dir') . '/' . $config->get('core_agent_full_name'); + $this->coreAgentDir = $config->get(ConfigKey::CORE_AGENT_DIRECTORY) . '/' . $config->get(ConfigKey::CORE_AGENT_FULL_NAME); $this->downloader = $downloader; } public function launch() : bool { - if (! $this->config->get('core_agent_launch')) { - $this->logger->debug("Not attempting to launch Core Agent due to 'core_agent_launch' setting."); + if (! $this->config->get(ConfigKey::CORE_AGENT_LAUNCH_ENABLED)) { + $this->logger->debug(sprintf( + "Not attempting to launch Core Agent due to '%s' setting.", + ConfigKey::CORE_AGENT_LAUNCH_ENABLED + )); return false; } if (! $this->verify()) { - if (! $this->config->get('core_agent_download')) { - $this->logger->debug( - "Not attempting to download Core Agent due to 'core_agent_download' setting." - ); + if (! $this->config->get(ConfigKey::CORE_AGENT_DOWNLOAD_ENABLED)) { + $this->logger->debug(sprintf( + "Not attempting to download Core Agent due to '%s' setting.", + ConfigKey::CORE_AGENT_DOWNLOAD_ENABLED + )); return false; } @@ -108,9 +114,9 @@ private function run() : bool { $this->logger->debug('Core Agent Launch in Progress'); try { - $logLevel = $this->config->get('log_level'); - $logFile = $this->config->get('log_file'); - $configFile = $this->config->get('config_file'); + $logLevel = $this->config->get(ConfigKey::CORE_AGENT_LOG_LEVEL); + $logFile = $this->config->get(ConfigKey::CORE_AGENT_LOG_FILE); + $configFile = $this->config->get(ConfigKey::CORE_AGENT_CONFIG_FILE); if ($logFile === null) { $logFile = '/dev/null'; @@ -136,7 +142,7 @@ private function run() : bool } $commandParts[] = '--socket'; - $commandParts[] = $this->config->get('socket_path'); + $commandParts[] = $this->config->get(ConfigKey::CORE_AGENT_SOCKET_PATH); $escapedCommand = implode(' ', array_map('escapeshellarg', $commandParts)); diff --git a/src/Logger/FilteredLogLevelDecorator.php b/src/Logger/FilteredLogLevelDecorator.php new file mode 100644 index 00000000..111905f8 --- /dev/null +++ b/src/Logger/FilteredLogLevelDecorator.php @@ -0,0 +1,57 @@ + 0, + LogLevel::INFO => 1, + LogLevel::NOTICE => 2, + LogLevel::WARNING => 3, + LogLevel::ERROR => 4, + LogLevel::CRITICAL => 5, + LogLevel::ALERT => 6, + LogLevel::EMERGENCY => 7, + ]; + + /** @var LoggerInterface */ + private $realLogger; + + /** @var int */ + private $minimumLogLevel; + + /** + * @param string $minimumLogLevel e.g. `emergency`, `error`, etc. - {@see \Psr\Log\LogLevel} + */ + public function __construct(LoggerInterface $realLogger, string $minimumLogLevel) + { + Assert::keyExists(self::LOG_LEVEL_ORDER, strtolower($minimumLogLevel)); + + $this->minimumLogLevel = self::LOG_LEVEL_ORDER[strtolower($minimumLogLevel)]; + $this->realLogger = $realLogger; + } + + /** {@inheritDoc} */ + public function log($level, $message, array $context = []) + { + if ($this->minimumLogLevel > self::LOG_LEVEL_ORDER[$level]) { + return; + } + + $this->realLogger->log($level, $message, $context); + } +} diff --git a/tests/Integration/AgentTest.php b/tests/Integration/AgentTest.php index 7358b427..1d88d31c 100644 --- a/tests/Integration/AgentTest.php +++ b/tests/Integration/AgentTest.php @@ -9,6 +9,7 @@ use Psr\Log\Test\TestLogger; use Scoutapm\Agent; use Scoutapm\Config; +use Scoutapm\Config\ConfigKey; use Scoutapm\Connector\SocketConnector; use Scoutapm\Extension\PotentiallyAvailableExtensionCapabilities; use function file_get_contents; @@ -64,7 +65,7 @@ public function testLoggingIsSent() : void 'monitor' => true, ]); - $connector = new MessageCapturingConnectorDelegator(new SocketConnector($config->get('socket_path'))); + $connector = new MessageCapturingConnectorDelegator(new SocketConnector($config->get(ConfigKey::CORE_AGENT_SOCKET_PATH))); $agent = Agent::fromConfig($config, $this->logger, $connector); diff --git a/tests/Unit/AgentTest.php b/tests/Unit/AgentTest.php index e6ca52e9..13e1a191 100644 --- a/tests/Unit/AgentTest.php +++ b/tests/Unit/AgentTest.php @@ -4,9 +4,13 @@ namespace Scoutapm\UnitTests; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; use Scoutapm\Agent; use Scoutapm\Config; +use Scoutapm\Config\ConfigKey; use Scoutapm\Events\Span\Span; use Scoutapm\Events\Tag\TagRequest; use function end; @@ -14,6 +18,38 @@ /** @covers \Scoutapm\Agent */ final class AgentTest extends TestCase { + public function testMinimumLogLevelCanBeSetOnConfigurationToSquelchNoisyLogMessages() : void + { + /** @var LoggerInterface&MockObject $logger */ + $logger = $this->createMock(LoggerInterface::class); + + $logger->expects(self::never()) + ->method('log'); + + $config = new Config(); + $config->set(ConfigKey::LOG_LEVEL, LogLevel::WARNING); + $config->set(ConfigKey::MONITORING_ENABLED, 'false'); + + $agent = Agent::fromConfig($config, $logger); + $agent->connect(); + } + + public function testLogMessagesAreLoggedWhenUsingDefaultConfiguration() : void + { + /** @var LoggerInterface&MockObject $logger */ + $logger = $this->createMock(LoggerInterface::class); + + $logger->expects(self::once()) + ->method('log') + ->with(LogLevel::DEBUG, 'Scout Core Agent Connected', []); + + $config = new Config(); + $config->set(ConfigKey::MONITORING_ENABLED, 'false'); + + $agent = Agent::fromConfig($config, $logger); + $agent->connect(); + } + public function testFullAgentSequence() : void { $agent = Agent::fromDefaults(); @@ -125,7 +161,7 @@ public function testEnabled() : void // but a config that has monitor = true, it is set $config = new Config(); - $config->set('monitor', 'true'); + $config->set(ConfigKey::MONITORING_ENABLED, 'true'); $enabledAgent = Agent::fromConfig($config); self::assertTrue($enabledAgent->enabled()); @@ -134,7 +170,7 @@ public function testEnabled() : void public function testIgnoredEndpoints() : void { $config = new Config(); - $config->set('ignore', ['/foo']); + $config->set(ConfigKey::IGNORED_ENDPOINTS, ['/foo']); $agent = Agent::fromConfig($config); diff --git a/tests/Unit/ConfigTest.php b/tests/Unit/ConfigTest.php index d3e2b93a..959a4814 100644 --- a/tests/Unit/ConfigTest.php +++ b/tests/Unit/ConfigTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\TestCase; use Scoutapm\Config; +use Scoutapm\Config\ConfigKey; use function putenv; /** @covers \Scoutapm\Config*/ @@ -16,15 +17,15 @@ public function testGetFallsBackToDefaults() : void $config = new Config(); // Provided by the DefaultConfig - self::assertSame('1.0', $config->get('api_version')); + self::assertSame('1.0', $config->get(ConfigKey::API_VERSION)); } public function testUserSettingsOverridesDefaults() : void { $config = new Config(); - $config->set('api_version', 'viauserconf'); + $config->set(ConfigKey::API_VERSION, 'viauserconf'); - self::assertSame('viauserconf', $config->get('api_version')); + self::assertSame('viauserconf', $config->get(ConfigKey::API_VERSION)); } public function testEnvOverridesAll() : void @@ -32,12 +33,12 @@ public function testEnvOverridesAll() : void $config = new Config(); // Set a user config. This won't be looked up - $config->set('api_version', 'viauserconf'); + $config->set(ConfigKey::API_VERSION, 'viauserconf'); // And set the env var putenv('SCOUT_API_VERSION=viaenvvar'); - self::assertSame('viaenvvar', $config->get('api_version')); + self::assertSame('viaenvvar', $config->get(ConfigKey::API_VERSION)); putenv('SCOUT_API_VERSION'); } @@ -47,8 +48,8 @@ public function testBooleanCoercionOfMonitor() : void $config = new Config(); // Set a user config. This won't be looked up - $config->set('monitor', 'true'); - self::assertTrue($config->get('monitor')); + $config->set(ConfigKey::MONITORING_ENABLED, 'true'); + self::assertTrue($config->get(ConfigKey::MONITORING_ENABLED)); } public function testJSONCoercionOfIgnore() : void @@ -56,12 +57,12 @@ public function testJSONCoercionOfIgnore() : void $config = new Config(); // Set a user config. This won't be looked up - $config->set('ignore', '["/foo", "/bar"]'); - self::assertSame(['/foo', '/bar'], $config->get('ignore')); + $config->set(ConfigKey::IGNORED_ENDPOINTS, '["/foo", "/bar"]'); + self::assertSame(['/foo', '/bar'], $config->get(ConfigKey::IGNORED_ENDPOINTS)); } public function testIgnoreDefaultsToEmptyArray() : void { - self::assertSame([], (new Config())->get('ignore')); + self::assertSame([], (new Config())->get(ConfigKey::IGNORED_ENDPOINTS)); } } diff --git a/tests/Unit/Logger/FilteredLogLevelDecoratorTest.php b/tests/Unit/Logger/FilteredLogLevelDecoratorTest.php new file mode 100644 index 00000000..cf0b90b6 --- /dev/null +++ b/tests/Unit/Logger/FilteredLogLevelDecoratorTest.php @@ -0,0 +1,72 @@ +decoratedLogger = $this->createMock(LoggerInterface::class); + } + + public function testLogMessagesBelowThresholdAreNotLogged() : void + { + $decorator = new FilteredLogLevelDecorator($this->decoratedLogger, LogLevel::NOTICE); + + $this->decoratedLogger + ->expects(self::never()) + ->method('log'); + + $decorator->info(uniqid('logMessage', true)); + } + + public function testLogMessagesAboveThresholdAreLogged() : void + { + $decorator = new FilteredLogLevelDecorator($this->decoratedLogger, LogLevel::NOTICE); + + $logMessage = uniqid('logMessage', true); + $context = [uniqid('foo', true) => uniqid('bar', true)]; + + $this->decoratedLogger + ->expects(self::once()) + ->method('log') + ->with(LogLevel::WARNING, $logMessage, $context); + + $decorator->warning($logMessage, $context); + } + + /** @return array> */ + public function invalidLogLevelProvider() : array + { + return [ + ['lizard'], + [''], + [uniqid('randomString', true)], + [str_repeat('a', 1024)], + ]; + } + + /** @dataProvider invalidLogLevelProvider */ + public function testInvalidLogLevelsAreRejected(string $invalidLogLevel) : void + { + $this->expectException(InvalidArgumentException::class); + new FilteredLogLevelDecorator($this->decoratedLogger, $invalidLogLevel); + } +}