diff --git a/README.md b/README.md index d9060bb..8d3e92e 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ $eppoClient = EppoClient::init( $cache // optional, must be an instance of PSR-16 SimpleCache\CacheInterface. If not passed, FileSystem cache will be used $httpClient // optional, must be an instance of PSR-18 ClientInterface. If not passed, Discovery will be used to find a suitable implementation $requestFactory // optional, must be an instance of PSR-17 Factory. If not passed, Discovery will be used to find a suitable implementation + $logger // optional, must be an instance of PSR-3 LoggerInterface for SDK logs ); ``` @@ -131,6 +132,9 @@ The `init` function accepts the following optional configuration arguments. | **`assignmentLogger`** | AssignmentLogger/IBanditLogger | Logs assignment events back to data warehoouse | `null` | | **`httpClient`** | ClientInterface | For making HTTP requests. If not passed, Discovery will attempt to autoload an applicable pacakge | `null` | | **`requestFactory`** | RequestFactoryInterface | Instance of PSR-17 Factory. If not passed, Discovery will be used to find a suitable implementation | null | +| **`logger`** | PSR-3 LoggerInterface | Logs SDK warnings/errors (replaces `syslog`/`error_log`) | `null` | + +To preserve the previous syslog behavior, pass a PSR-3 logger configured for syslog, such as Monolog with a SyslogHandler. ## Assignment logger @@ -216,6 +220,7 @@ $eppoClient = EppoClient::init( $cache // optional, must be an instance of PSR-16 SimpleInterface. If not passed, FileSystem cache will be used $httpClient // optional, must be an instance of PSR-18 ClientInterface. If not passed, Discovery will be used to find a suitable implementation $requestFactory // optional, must be an instance of PSR-17 Factory. If not passed, Discovery will be used to find a suitable implementation + $logger // optional, must be an instance of PSR-3 LoggerInterface for SDK logs ); $eppoClient->startPolling(); diff --git a/composer.json b/composer.json index c125876..27a6851 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "php": "^8.1", "ext-json": "*", "psr/simple-cache": "3.*", + "psr/log": "^2.0|^3.0", "shrikeh/teapot": "^2.3", "composer/semver": "^3.4", "php-http/discovery": "^1.17", diff --git a/composer.lock b/composer.lock index d2819bc..205c964 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ce4ac74d50ed43c2c04e6bb32760a272", + "content-hash": "e792ecf4e40a93549007d04d80bea305", "packages": [ { "name": "composer/semver", @@ -375,16 +375,16 @@ }, { "name": "psr/log", - "version": "3.0.0", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { @@ -419,9 +419,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.0" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2021-07-14T16:46:02+00:00" + "time": "2024-09-11T13:17:53+00:00" }, { "name": "psr/simple-cache", @@ -4563,5 +4563,5 @@ "ext-pcntl": "*", "ext-sockets": "*" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/src/Config/ConfigurationLoader.php b/src/Config/ConfigurationLoader.php index 6d7a7e1..fa9bd74 100644 --- a/src/Config/ConfigurationLoader.php +++ b/src/Config/ConfigurationLoader.php @@ -7,14 +7,20 @@ use Eppo\DTO\FlagConfigResponse; use Eppo\Exception\HttpRequestException; use Eppo\Exception\InvalidApiKeyException; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\NullLogger; -class ConfigurationLoader +class ConfigurationLoader implements LoggerAwareInterface { + use LoggerAwareTrait; + public function __construct( private readonly APIRequestWrapper $apiRequestWrapper, public readonly ConfigurationStore $configurationStore, private readonly int $cacheAgeLimitMillis = 30 * 1000 ) { + $this->setLogger(new NullLogger()); } /** @@ -46,7 +52,9 @@ public function fetchAndStoreConfiguration(?string $flagETag): void $responseData = json_decode($response->body, true); if ($responseData === null) { - syslog(LOG_WARNING, "[Eppo SDK] Empty or invalid response from the configuration server."); + $this->logger->warning( + '[Eppo SDK] Empty or invalid response from the configuration server.' + ); return; } $fcr = FlagConfigResponse::fromArray($responseData); @@ -79,7 +87,9 @@ public function fetchAndStoreConfiguration(?string $flagETag): void // Configuration object. $banditResource = $this->apiRequestWrapper->getBandits(); if (!$banditResource?->body) { - syslog(E_ERROR, "[Eppo SDK] Empty or invalid bandit response from the configuration server."); + $this->logger->error( + '[Eppo SDK] Empty or invalid bandit response from the configuration server.' + ); } else { $banditResponse = new ConfigResponse($banditResource->body, date('c'), $banditResource->eTag); } diff --git a/src/Config/ConfigurationStore.php b/src/Config/ConfigurationStore.php index 410b663..43e2bba 100644 --- a/src/Config/ConfigurationStore.php +++ b/src/Config/ConfigurationStore.php @@ -3,16 +3,22 @@ namespace Eppo\Config; use Eppo\DTO\ConfigurationWire\ConfigurationWire; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\NullLogger; use Psr\SimpleCache\CacheInterface; use Throwable; -class ConfigurationStore +class ConfigurationStore implements LoggerAwareInterface { + use LoggerAwareTrait; + private const CONFIG_KEY = "EPPO_configuration_v1"; private ?Configuration $configuration = null; public function __construct(private readonly CacheInterface $cache) { + $this->setLogger(new NullLogger()); } public function getConfiguration(): Configuration @@ -37,7 +43,10 @@ public function getConfiguration(): Configuration return $this->configuration; } catch (Throwable $e) { // Safe to ignore as the const `CONFIG_KEY` contains no invalid characters - syslog(LOG_ERR, "[Eppo SDK] Error loading config from cache " . $e->getMessage()); + $this->logger->error( + '[Eppo SDK] Error loading config from cache ' . $e->getMessage(), + ['exception' => $e] + ); return Configuration::emptyConfig(); } } @@ -49,7 +58,10 @@ public function setConfiguration(Configuration $configuration): void $this->cache->set(self::CONFIG_KEY, json_encode($configuration->toConfigurationWire()->toArray())); } catch (Throwable $e) { // Safe to ignore as the const `CONFIG_KEY` contains no invalid characters - syslog(LOG_ERR, "[Eppo SDK] Error loading config from cache " . $e->getMessage()); + $this->logger->error( + '[Eppo SDK] Error saving config to cache ' . $e->getMessage(), + ['exception' => $e] + ); } } } diff --git a/src/DTO/Bandit/AttributeSet.php b/src/DTO/Bandit/AttributeSet.php index b64f072..e1a50a3 100644 --- a/src/DTO/Bandit/AttributeSet.php +++ b/src/DTO/Bandit/AttributeSet.php @@ -32,11 +32,6 @@ public function __construct( foreach ($numericAttributes as $key => $value) { if (self::isNumberType($value)) { $numeric[$key] = $value; - } else { - syslog( - LOG_WARNING, - "[Eppo SDK] non-numeric attribute passed in `\$numericAttributes` for key $key" - ); } } $this->numericAttributes = $numeric; @@ -51,8 +46,6 @@ public static function fromArray(array $attributes): self $numericAttributes[$key] = $value; } elseif (self::isCategoricalType($value)) { $categoricalAttributes[$key] = $value; - } else { - syslog(LOG_WARNING, "[Eppo SDK] Unsupported attribute type: " . gettype($value)); } } return new self($numericAttributes, $categoricalAttributes); diff --git a/src/DTO/Bandit/Bandit.php b/src/DTO/Bandit/Bandit.php index c4951f7..9c9fc03 100644 --- a/src/DTO/Bandit/Bandit.php +++ b/src/DTO/Bandit/Bandit.php @@ -30,10 +30,6 @@ public static function fromArray(array $arr): Bandit $updatedAt = new DateTime($arr['updatedAt']); } } catch (\Exception $e) { - syslog( - LOG_WARNING, - "[Eppo SDK] invalid timestamp for bandit model {$arr['updatedAt']}: " . $e->getMessage() - ); $updatedAt = new DateTime(); } finally { return new Bandit( diff --git a/src/EppoClient.php b/src/EppoClient.php index 2d37ce9..7230832 100644 --- a/src/EppoClient.php +++ b/src/EppoClient.php @@ -31,6 +31,9 @@ use Http\Discovery\Psr18ClientDiscovery; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerInterface as PsrLoggerInterface; +use Psr\Log\NullLogger; use Psr\SimpleCache\CacheInterface; class EppoClient @@ -44,6 +47,7 @@ class EppoClient private static ?EppoClient $instance = null; private RuleEvaluator $evaluator; private IBanditEvaluator $banditEvaluator; + private PsrLoggerInterface $logger; /** * @param ConfigurationStore $configurationStore @@ -60,9 +64,11 @@ protected function __construct( private readonly ?LoggerInterface $eventLogger = null, private readonly ?bool $isGracefulMode = true, IBanditEvaluator $banditEvaluator = null, + ?PsrLoggerInterface $logger = null, ) { $this->evaluator = new RuleEvaluator(); $this->banditEvaluator = $banditEvaluator ?? new BanditEvaluator(); + $this->logger = $logger ?? new NullLogger(); } /** @@ -75,6 +81,7 @@ protected function __construct( * use Discovery to locate a suitable implementation in the project. * @param RequestFactoryInterface|null $requestFactory optional PSR-17 Request Factory implementation. If none is * provided, EppoClient will use Discovery + * @param PsrLoggerInterface|null $logger optional PSR-3 logger for SDK diagnostics. * @throws EppoClientInitializationException * @throws EppoClientException */ @@ -88,6 +95,7 @@ public static function init( ?bool $isGracefulMode = true, ?PollingOptions $pollingOptions = null, ?bool $throwOnFailedInit = false, + ?PsrLoggerInterface $logger = null, ): EppoClient { // Get SDK metadata to pass as params in the http client. $sdkData = new SDKData(); @@ -100,7 +108,10 @@ public static function init( $cache = (new DefaultCacheFactory())->create(); } + $psrLogger = $logger ?? new NullLogger(); + $configStore = new ConfigurationStore($cache); + $configStore->setLogger($psrLogger); if (!$httpClient) { $httpClient = Psr18ClientDiscovery::find(); @@ -134,6 +145,7 @@ public static function init( } $configLoader = new ConfigurationLoader($apiWrapper, $configStore, $cacheAgeLimit); + $configLoader->setLogger($psrLogger); $poller = new Poller( $interval, @@ -142,6 +154,7 @@ function () use ($configLoader) { $configLoader->reloadConfiguration(); } ); + $poller->setLogger($psrLogger); self::$instance = self::createAndInitClient( $configStore, @@ -149,7 +162,8 @@ function () use ($configLoader) { $poller, $assignmentLogger, $isGracefulMode, - throwOnFailedInit: $throwOnFailedInit + throwOnFailedInit: $throwOnFailedInit, + logger: $psrLogger, ); return self::$instance; @@ -166,7 +180,10 @@ private static function createAndInitClient( ?bool $isGracefulMode, ?IBanditEvaluator $banditEvaluator = null, ?bool $throwOnFailedInit = false, + ?PsrLoggerInterface $logger = null, ): EppoClient { + $psrLogger = $logger ?? new NullLogger(); + try { $configLoader->reloadConfigurationIfExpired(); } catch (Exception | HttpRequestException | InvalidApiKeyException $e) { @@ -176,10 +193,18 @@ private static function createAndInitClient( $message ); } else { - syslog(LOG_INFO, "[Eppo SDK] " . $message); + $psrLogger->warning('[Eppo SDK] ' . $message, ['exception' => $e]); } } - return new self($configStore, $configLoader, $poller, $assignmentLogger, $isGracefulMode, $banditEvaluator); + return new self( + $configStore, + $configLoader, + $poller, + $assignmentLogger, + $isGracefulMode, + $banditEvaluator, + $psrLogger, + ); } /** @@ -359,7 +384,7 @@ private function getAssignmentDetail( $flag = $config->getFlag($flagKey); if (!$flag) { - syslog(LOG_WARNING, "[EPPO SDK] No assigned variation; flag not found {$flagKey}"); + $this->logger->warning("[Eppo SDK] No assigned variation; flag not found {$flagKey}"); return null; } @@ -376,12 +401,14 @@ private function getAssignmentDetail( ) { $actualType = gettype($computedVariation->value); $eVarType = $expectedVariationType->value; - syslog(LOG_ERR, "[EPPO SDK] Variation does not have the expected type, {$eVarType}; found {$actualType}"); + $this->logger->error( + "[Eppo SDK] Variation does not have the expected type, {$eVarType}; found {$actualType}" + ); return null; } if (!$flag->enabled) { - syslog(LOG_INFO, '[EPPO SDK] No assigned variation; flag is disabled.'); + $this->logger->info('[EPPO SDK] No assigned variation; flag is disabled.'); return null; } @@ -405,7 +432,10 @@ private function getAssignmentDetail( ) ); } catch (Exception $exception) { - error_log('[Eppo SDK] Error logging assignment event: ' . $exception->getMessage()); + $this->logger->error( + '[Eppo SDK] Error logging assignment event: ' . $exception->getMessage(), + ['exception' => $exception] + ); } } @@ -487,7 +517,10 @@ public function getBanditAction( return $this->getBanditDetail($flagKey, $subjectKey, $subject, $actionContexts, $defaultValue); } catch (EppoException $e) { if ($this->isGracefulMode) { - error_log('[Eppo SDK] Error selecting bandit action: ' . $e->getMessage()); + $this->logger->error( + '[Eppo SDK] Error selecting bandit action: ' . $e->getMessage(), + ['exception' => $e] + ); return new BanditResult($defaultValue); } else { throw EppoClientException::from($e); @@ -530,7 +563,10 @@ private function getBanditDetail( return new BanditResult($defaultValue); } } catch (EppoException $e) { - syslog(LOG_WARNING, "[Eppo SDK] Error computing experiment assignment: " . $e->getMessage()); + $this->logger->warning( + '[Eppo SDK] Error computing experiment assignment: ' . $e->getMessage(), + ['exception' => $e] + ); return new BanditResult($defaultValue); } @@ -567,7 +603,10 @@ private function getBanditDetail( try { $this->eventLogger->logBanditAction($banditActionLog); } catch (Exception $exception) { - syslog(LOG_WARNING, "[Eppo SDK] Error in logging bandit action: " . $exception->getMessage()); + $this->logger->warning( + '[Eppo SDK] Error in logging bandit action: ' . $exception->getMessage(), + ['exception' => $exception] + ); } } return new BanditResult($variation, $result->selectedAction); @@ -601,7 +640,10 @@ public function fetchAndActivateConfiguration(): void $this->configurationLoader->fetchAndStoreConfiguration(null); } catch (HttpRequestException | InvalidApiKeyException | InvalidConfigurationException $e) { if ($this->isGracefulMode) { - error_log('[Eppo SDK] Error fetching configuration ' . $e->getMessage()); + $this->logger->error( + '[Eppo SDK] Error fetching configuration ' . $e->getMessage(), + ['exception' => $e] + ); } else { throw EppoClientException::from($e); } @@ -633,7 +675,10 @@ private function handleException( array|bool|float|int|string|null $defaultValue ): array|bool|float|int|string|null { if ($this->isGracefulMode) { - error_log('[Eppo SDK] Error getting assignment: ' . $exception->getMessage()); + $this->logger->error( + '[Eppo SDK] Error getting assignment: ' . $exception->getMessage(), + ['exception' => $exception] + ); return $defaultValue; } throw EppoClientException::from($exception); @@ -650,6 +695,7 @@ private function handleException( * @param bool|null $isGracefulMode * @param IBanditEvaluator|null $banditEvaluator * @param bool|null $throwOnFailedInit + * @param PsrLoggerInterface|null $psrLogger * @return EppoClient * @throws EppoClientInitializationException */ @@ -661,7 +707,16 @@ public static function createTestClient( ?bool $isGracefulMode = false, ?IBanditEvaluator $banditEvaluator = null, ?bool $throwOnFailedInit = true, + ?PsrLoggerInterface $psrLogger = null, ): EppoClient { + $psrLogger = $psrLogger ?? new NullLogger(); + + $configStore->setLogger($psrLogger); + $configurationLoader->setLogger($psrLogger); + if ($poller instanceof LoggerAwareInterface) { + $poller->setLogger($psrLogger); + } + return self::createAndInitClient( $configStore, $configurationLoader, @@ -669,7 +724,8 @@ public static function createTestClient( $logger, $isGracefulMode, $banditEvaluator, - throwOnFailedInit: $throwOnFailedInit + throwOnFailedInit: $throwOnFailedInit, + logger: $psrLogger ); } diff --git a/src/Poller.php b/src/Poller.php index 2124a1c..2f35e4e 100644 --- a/src/Poller.php +++ b/src/Poller.php @@ -3,9 +3,14 @@ namespace Eppo; use Eppo\Exception\HttpRequestException; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\NullLogger; -class Poller implements PollerInterface +class Poller implements PollerInterface, LoggerAwareInterface { + use LoggerAwareTrait; + /** @var bool */ private $stopped = false; @@ -28,6 +33,7 @@ public function __construct(int $interval, int $jitterMillis, callable $callback $this->interval = $interval; $this->jitterMillis = $jitterMillis; $this->callback = $callback; + $this->setLogger(new NullLogger()); } public function start(): void @@ -53,7 +59,10 @@ private function poll(): void if (!$error->isRecoverable) { $this->stop(); } - error_log("[Eppo SDK] Error polling configurations: " . $error->getMessage()); + $this->logger->error( + '[Eppo SDK] Error polling configurations: ' . $error->getMessage(), + ['exception' => $error] + ); } $intervalWithJitter = $this->interval - mt_rand(0, $this->jitterMillis);