From c1fca88fc3f5be20218d92922add07726161458b Mon Sep 17 00:00:00 2001 From: Arun Bhalla Date: Thu, 25 May 2017 02:34:42 -0700 Subject: [PATCH 1/5] [CH3370] Support publishing events via ld-relay --- composer.json | 2 +- src/LaunchDarkly/CurlEventPublisher.php | 63 ++++++++++++++++++++ src/LaunchDarkly/EventProcessor.php | 76 +++++++----------------- src/LaunchDarkly/EventPublisher.php | 18 ++++++ src/LaunchDarkly/LDClient.php | 21 +++++-- src/LaunchDarkly/RelayEventPublisher.php | 52 ++++++++++++++++ 6 files changed, 173 insertions(+), 59 deletions(-) create mode 100644 src/LaunchDarkly/CurlEventPublisher.php create mode 100644 src/LaunchDarkly/EventPublisher.php create mode 100644 src/LaunchDarkly/RelayEventPublisher.php diff --git a/composer.json b/composer.json index 2ed48998a..8f18b4c7b 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "zendframework/zend-serializer": "^2.7" }, "suggest": { - "guzzlehttp/guzzle": "(^6.2.1) Required when using the default FeatureRequester", + "guzzlehttp/guzzle": "(^6.2.1) Required when using RelayEventPublisher or the default FeatureRequester", "kevinrob/guzzle-cache-middleware": "(^1.4.1) Recommended for performance when using the default FeatureRequester", "predis/predis": "(^1.0) Required when using LDDFeatureRequester" }, diff --git a/src/LaunchDarkly/CurlEventPublisher.php b/src/LaunchDarkly/CurlEventPublisher.php new file mode 100644 index 000000000..d14a6e152 --- /dev/null +++ b/src/LaunchDarkly/CurlEventPublisher.php @@ -0,0 +1,63 @@ +_sdkKey = $apiKey; + + $eventsUri = LDClient::DEFAULT_EVENTS_URI; + if (isset($options['events_uri'])) { + $eventsUri = $options['events_uri']; + } + $url = parse_url(rtrim($eventsUri,'/')); + $this->_host = $url['host']; + $this->_ssl = $url['scheme'] === 'https'; + if (isset($url['port'])) { + $this->_port = $url['port']; + } + else { + $this->_port = $this->_ssl ? 443 : 80; + } + if (isset($url['path'])) { + $this->_path = $url['path']; + } + else { + $this->_path = ''; + } + + if (array_key_exists('curl', $options)) { + $this->_curl = $options['curl']; + } + } + + public function publish($payload) { + $args = $this->createArgs($payload); + + return $this->makeRequest($args); + } + + private function createArgs($payload) { + $scheme = $this->_ssl ? "https://" : "http://"; + $args = " -X POST"; + $args.= " -H 'Content-Type: application/json'"; + $args.= " -H " . escapeshellarg("Authorization: " . $this->_sdkKey); + $args.= " -H 'User-Agent: PHPClient/" . LDClient::VERSION . "'"; + $args.= " -H 'Accept: application/json'"; + $args.= " -d " . escapeshellarg($payload); + $args.= " " . escapeshellarg($scheme . $this->_host . ":" . $this->_port . $this->_path . "/bulk"); + return $args; + } + + private function makeRequest($args) { + $cmd = $this->_curl . " " . $args . ">> /dev/null 2>&1 &"; + shell_exec($cmd); + return true; + } +} \ No newline at end of file diff --git a/src/LaunchDarkly/EventProcessor.php b/src/LaunchDarkly/EventProcessor.php index cfa74043e..621789097 100644 --- a/src/LaunchDarkly/EventProcessor.php +++ b/src/LaunchDarkly/EventProcessor.php @@ -6,44 +6,13 @@ */ class EventProcessor { - private $_sdkKey; + private $_eventPublisher; private $_queue; private $_capacity; private $_timeout; - private $_host; - private $_port; - private $_ssl; - private $_curl = '/usr/bin/env curl'; public function __construct($apiKey, $options = array()) { - $this->_sdkKey = $apiKey; - if (!isset($options['events_uri'])) { - $this->_host = 'events.launchdarkly.com'; - $this->_port = 443; - $this->_ssl = true; - $this->_path = ''; - } - else { - $url = parse_url(rtrim($options['events_uri'],'/')); - $this->_host = $url['host']; - $this->_ssl = $url['scheme'] === 'https'; - if (isset($url['port'])) { - $this->_port = $url['port']; - } - else { - $this->_port = $this->_ssl ? 443 : 80; - } - if (isset($url['path'])) { - $this->_path = $url['path']; - } - else { - $this->_path = ''; - } - } - - if (array_key_exists('curl', $options)) { - $this->_curl = $options['curl']; - } + $this->_eventPublisher = $this->getEventPublisher($apiKey, $options); $this->_capacity = $options['capacity']; $this->_timeout = $options['timeout']; @@ -76,28 +45,29 @@ protected function flush() { $payload = json_encode($this->_queue); - $args = $this->createArgs($payload); - - return $this->makeRequest($args); + return $this->_eventPublisher->publish($payload); } - private function createArgs($payload) { - $scheme = $this->_ssl ? "https://" : "http://"; - $args = " -X POST"; - $args.= " -H 'Content-Type: application/json'"; - $args.= " -H " . escapeshellarg("Authorization: " . $this->_sdkKey); - $args.= " -H 'User-Agent: PHPClient/" . LDClient::VERSION . "'"; - $args.= " -H 'Accept: application/json'"; - $args.= " -d " . escapeshellarg($payload); - $args.= " " . escapeshellarg($scheme . $this->_host . ":" . $this->_port . $this->_path . "/bulk"); - return $args; - } - - private function makeRequest($args) { - $cmd = $this->_curl . " " . $args . ">> /dev/null 2>&1 &"; - shell_exec($cmd); - return true; - } + /** + * @param string $sdkKey + * @param mixed[] $options + * @return EventPublisher + */ + private function getEventPublisher($sdkKey, array $options) + { + if (isset($options['event_publisher']) && $options['event_publisher'] instanceof EventPublisher) { + return $options['event_publisher']; + } + if (isset($options['event_publisher_class'])) { + $eventPublisherClass = $options['event_publisher_class']; + } else { + $eventPublisherClass = CurlEventPublisher::class; + } + if (!is_a($eventPublisherClass, EventPublisher::class, true)) { + throw new \InvalidArgumentException; + } + return new $eventPublisherClass($sdkKey, $options); + } } \ No newline at end of file diff --git a/src/LaunchDarkly/EventPublisher.php b/src/LaunchDarkly/EventPublisher.php new file mode 100644 index 000000000..63b9446e8 --- /dev/null +++ b/src/LaunchDarkly/EventPublisher.php @@ -0,0 +1,18 @@ +_sdkKey = $sdkKey; @@ -51,6 +57,11 @@ public function __construct($sdkKey, $options = array()) { } else { $this->_baseUri = rtrim($options['base_uri'], '/'); } + if (!isset($options['events_uri'])) { + $this->_eventsUri = self::DEFAULT_EVENTS_URI; + } else { + $this->_eventsUri = rtrim($options['events_uri'], '/'); + } if (isset($options['send_events'])) { $this->_send_events = $options['send_events']; } @@ -82,15 +93,15 @@ public function __construct($sdkKey, $options = array()) { $this->_eventProcessor = new EventProcessor($sdkKey, $options); - $this->_featureRequester = $this->getFeatureRequester($options, $sdkKey); + $this->_featureRequester = $this->getFeatureRequester($sdkKey, $options); } /** - * @param mixed[] $options * @param string $sdkKey + * @param mixed[] $options * @return FeatureRequester */ - private function getFeatureRequester(array $options, $sdkKey) + private function getFeatureRequester($sdkKey, array $options) { if (isset($options['feature_requester']) && $options['feature_requester'] instanceof FeatureRequester) { return $options['feature_requester']; diff --git a/src/LaunchDarkly/RelayEventPublisher.php b/src/LaunchDarkly/RelayEventPublisher.php new file mode 100644 index 000000000..fcffe656e --- /dev/null +++ b/src/LaunchDarkly/RelayEventPublisher.php @@ -0,0 +1,52 @@ +_sdkKey = $apiKey; + $this->_logger = $options['logger']; + if (isset($options['events_uri'])) { + $this->_eventsUri = $options['events_uri']; + } else { + $this->_eventsUri = LDClient::DEFAULT_EVENTS_URI; + } + $this->_requestOptions = [ + 'headers' => [ + 'Content-Type' => 'application/json', + 'Authorization' => $this->_sdkKey, + 'User-Agent' => 'PHPClient/' . LDClient::VERSION, + 'Accept' => 'application/json' + ], + 'timeout' => $options['timeout'], + 'connect_timeout' => $options['connect_timeout'] + ]; + } + + public function publish($payload) { + $client = new Client(['base_uri' => $this->_eventsUri]); + + try { + $options = $this->_requestOptions; + $options['body'] = $payload; + $response = $client->request('POST', '/bulk', $options); + + return $response->getStatusCode() < 300; + } catch (\Exception $e) { + $this->_logger->warning("RelayEventPublisher::publish caught $e"); + return false; + } + } +} \ No newline at end of file From ae90fe1d9810968fa58b4f96b1f424c5a48b7286 Mon Sep 17 00:00:00 2001 From: Arun Bhalla Date: Thu, 25 May 2017 12:13:44 -0700 Subject: [PATCH 2/5] Rename `apiKey` to `sdkKey` --- src/LaunchDarkly/ApcLDDFeatureRequester.php | 4 ++-- src/LaunchDarkly/CurlEventPublisher.php | 4 ++-- src/LaunchDarkly/EventProcessor.php | 4 ++-- src/LaunchDarkly/LDDFeatureRequester.php | 6 +++--- src/LaunchDarkly/RelayEventPublisher.php | 4 ++-- tests/LDDFeatureRequesterTest.php | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/LaunchDarkly/ApcLDDFeatureRequester.php b/src/LaunchDarkly/ApcLDDFeatureRequester.php index ad4a36af7..d6d56e005 100644 --- a/src/LaunchDarkly/ApcLDDFeatureRequester.php +++ b/src/LaunchDarkly/ApcLDDFeatureRequester.php @@ -13,8 +13,8 @@ class ApcLDDFeatureRequester extends LDDFeatureRequester { protected $_expiration = 30; - function __construct($baseUri, $apiKey, $options) { - parent::__construct($baseUri, $apiKey, $options); + function __construct($baseUri, $sdkKey, $options) { + parent::__construct($baseUri, $sdkKey, $options); if (isset($options['apc_expiration'])) { $this->_expiration = (int)$options['apc_expiration']; diff --git a/src/LaunchDarkly/CurlEventPublisher.php b/src/LaunchDarkly/CurlEventPublisher.php index d14a6e152..e898edbed 100644 --- a/src/LaunchDarkly/CurlEventPublisher.php +++ b/src/LaunchDarkly/CurlEventPublisher.php @@ -9,8 +9,8 @@ class CurlEventPublisher implements EventPublisher private $_ssl; private $_curl = '/usr/bin/env curl'; - function __construct($apiKey, array $options = array()) { - $this->_sdkKey = $apiKey; + function __construct($sdkKey, array $options = array()) { + $this->_sdkKey = $sdkKey; $eventsUri = LDClient::DEFAULT_EVENTS_URI; if (isset($options['events_uri'])) { diff --git a/src/LaunchDarkly/EventProcessor.php b/src/LaunchDarkly/EventProcessor.php index 621789097..af6ea81ab 100644 --- a/src/LaunchDarkly/EventProcessor.php +++ b/src/LaunchDarkly/EventProcessor.php @@ -11,8 +11,8 @@ class EventProcessor { private $_capacity; private $_timeout; - public function __construct($apiKey, $options = array()) { - $this->_eventPublisher = $this->getEventPublisher($apiKey, $options); + public function __construct($sdkKey, $options = array()) { + $this->_eventPublisher = $this->getEventPublisher($sdkKey, $options); $this->_capacity = $options['capacity']; $this->_timeout = $options['timeout']; diff --git a/src/LaunchDarkly/LDDFeatureRequester.php b/src/LaunchDarkly/LDDFeatureRequester.php index 67142b538..dd5ec7262 100644 --- a/src/LaunchDarkly/LDDFeatureRequester.php +++ b/src/LaunchDarkly/LDDFeatureRequester.php @@ -7,7 +7,7 @@ class LDDFeatureRequester implements FeatureRequester { protected $_baseUri; - protected $_apiKey; + protected $_sdkKey; protected $_options; protected $_features_key; /** @var LoggerInterface */ @@ -15,9 +15,9 @@ class LDDFeatureRequester implements FeatureRequester { /** @var ClientInterface */ private $_connection; - function __construct($baseUri, $apiKey, $options) { + function __construct($baseUri, $sdkKey, $options) { $this->_baseUri = $baseUri; - $this->_apiKey = $apiKey; + $this->_sdkKey = $sdkKey; if (!isset($options['redis_host'])) { $options['redis_host'] = 'localhost'; } diff --git a/src/LaunchDarkly/RelayEventPublisher.php b/src/LaunchDarkly/RelayEventPublisher.php index fcffe656e..8b59dc992 100644 --- a/src/LaunchDarkly/RelayEventPublisher.php +++ b/src/LaunchDarkly/RelayEventPublisher.php @@ -15,8 +15,8 @@ class RelayEventPublisher implements EventPublisher /** @var mixed[] */ private $_requestOptions; - function __construct($apiKey, array $options = array()) { - $this->_sdkKey = $apiKey; + function __construct($sdkKey, array $options = array()) { + $this->_sdkKey = $sdkKey; $this->_logger = $options['logger']; if (isset($options['events_uri'])) { $this->_eventsUri = $options['events_uri']; diff --git a/tests/LDDFeatureRequesterTest.php b/tests/LDDFeatureRequesterTest.php index 97bfcc428..2fb2d7d07 100644 --- a/tests/LDDFeatureRequesterTest.php +++ b/tests/LDDFeatureRequesterTest.php @@ -29,7 +29,7 @@ protected function setUp() public function testGet() { - $sut = new LDDFeatureRequester('example.com', 'MyApiKey', [ + $sut = new LDDFeatureRequester('example.com', 'MySdkKey', [ 'logger' => $this->logger, 'predis_client' => $this->predisClient, ]); From 384a406f63bc5bc7100c3d9745d19af2203d0bf1 Mon Sep 17 00:00:00 2001 From: Arun Bhalla Date: Thu, 25 May 2017 15:09:27 -0700 Subject: [PATCH 3/5] Add documentation for GuzzleEventPublisher and CurlEventPublisher --- README.md | 29 +++++++++++++++---- composer.json | 2 +- src/LaunchDarkly/CurlEventPublisher.php | 10 +++++++ src/LaunchDarkly/EventPublisher.php | 3 ++ ...Publisher.php => GuzzleEventPublisher.php} | 14 +++++++-- 5 files changed, 49 insertions(+), 9 deletions(-) rename src/LaunchDarkly/{RelayEventPublisher.php => GuzzleEventPublisher.php} (69%) diff --git a/README.md b/README.md index db889dd62..368d8ed30 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Require Guzzle as a dependency: It will then be used as the default way of fetching flags. -With Guzzle, you could persist your cache somewhere other than the default in-memory store, like Memcached or Redis. You could then specify your cache when initializing the client with the [cache option](https://github.com/launchdarkly/php-client/blob/master/src/LaunchDarkly/LDClient.php#L42). +With Guzzle, you could persist your cache somewhere other than the default in-memory store, like Memcached or Redis. You could then specify your cache when initializing the client with the [cache option](https://github.com/launchdarkly/php-client/blob/master/src/LaunchDarkly/LDClient.php#L44). $client = new LaunchDarkly\LDClient("YOUR_SDK_KEY", array("cache" => $cacheStorage)); @@ -66,15 +66,32 @@ With Guzzle, you could persist your cache somewhere other than the default in-me Using LD-Relay ============== -* Setup [ld-relay](https://github.com/launchdarkly/ld-relay) in [daemon-mode](https://github.com/launchdarkly/ld-relay#redis-storage-and-daemon-mode) with Redis +The LaunchDarkly Relay Proxy ([ld-relay](https://github.com/launchdarkly/ld-relay)) consumes the LaunchDarkly streaming API and can update +a Redis cache operating in your production environment. The ld-relay offers many benefits such as performance and feature flag consistency. With PHP applications, we strongly recommend setting up ld-relay with a Redis store. -* Require Predis as a dependency: +1. Set up ld-relay in [daemon-mode](https://github.com/launchdarkly/ld-relay#redis-storage-and-daemon-mode) with Redis - php composer.phar require "predis/predis:1.0.*" +2. Require Predis as a dependency: -* Create the LDClient with the Redis feature requester as an option: + php composer.phar require "predis/predis:1.0.*" - $client = new LaunchDarkly\LDClient("your_sdk_key", ['feature_requester_class' => 'LaunchDarkly\LDDFeatureRequester', 'redis_host' => 'your.redis.host', 'redis_port' => 6379]); +3. Create the LDClient with the Redis feature requester as an option: + + $client = new LaunchDarkly\LDClient("your_sdk_key", [ + 'feature_requester_class' => 'LaunchDarkly\LDDFeatureRequester', + 'redis_host' => 'your.redis.host', + 'redis_port' => 6379 + ]); + +4. If ld-relay is configured for [event forwarding](https://github.com/launchdarkly/ld-relay#event-forwarding), you can configure the LDClient to publish events to ld-relay instead of directly to `events.launchdarkly.com`. Using `GuzzleEventPublisher` with ld-relay event forwarding can be an efficient alternative to the default `curl`-based event publishing. + + $client = new LaunchDarkly\LDClient("your_sdk_key", [ + 'event_publisher_class' => 'LaunchDarkly\GuzzleEventPublisher', + 'events_uri' => 'http://your-ldrelay-host:8030', + 'feature_requester_class' => 'LaunchDarkly\LDDFeatureRequester', + 'redis_host' => 'your.redis.host', + 'redis_port' => 6379 + ]); Testing ------- diff --git a/composer.json b/composer.json index 8f18b4c7b..28d09d16e 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "zendframework/zend-serializer": "^2.7" }, "suggest": { - "guzzlehttp/guzzle": "(^6.2.1) Required when using RelayEventPublisher or the default FeatureRequester", + "guzzlehttp/guzzle": "(^6.2.1) Required when using GuzzleEventPublisher or the default FeatureRequester", "kevinrob/guzzle-cache-middleware": "(^1.4.1) Recommended for performance when using the default FeatureRequester", "predis/predis": "(^1.0) Required when using LDDFeatureRequester" }, diff --git a/src/LaunchDarkly/CurlEventPublisher.php b/src/LaunchDarkly/CurlEventPublisher.php index e898edbed..96131ed55 100644 --- a/src/LaunchDarkly/CurlEventPublisher.php +++ b/src/LaunchDarkly/CurlEventPublisher.php @@ -1,6 +1,16 @@ getStatusCode() < 300; } catch (\Exception $e) { - $this->_logger->warning("RelayEventPublisher::publish caught $e"); + $this->_logger->warning("GuzzleEventPublisher::publish caught $e"); return false; } } From a57b9a9c05565fe8f020ea4b880daf8addb3feb8 Mon Sep 17 00:00:00 2001 From: Arun Bhalla Date: Thu, 25 May 2017 15:47:41 -0700 Subject: [PATCH 4/5] Release version 2.2.0-beta1 --- VERSION | 2 +- src/LaunchDarkly/LDClient.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index eca07e4c1..e24c24656 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1.2 +2.2.0-beta1 diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 743a798e9..75b98ee7a 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -11,7 +11,7 @@ class LDClient { const DEFAULT_BASE_URI = 'https://app.launchdarkly.com'; const DEFAULT_EVENTS_URI = 'https://events.launchdarkly.com'; - const VERSION = '2.1.2'; + const VERSION = '2.2.0-beta1'; /** @var string */ protected $_sdkKey; From 2a99ad3b3d1a1494e5c9640af143c46518e19d44 Mon Sep 17 00:00:00 2001 From: Arun Bhalla Date: Mon, 5 Jun 2017 18:16:35 -0700 Subject: [PATCH 5/5] Release version 2.2.0 --- CHANGELOG.md | 6 ++++++ VERSION | 2 +- src/LaunchDarkly/LDClient.php | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd9beb412..8cd2db97c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to the LaunchDarkly PHP SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [2.2.0] - 2017-06-05 +### Added +- Support for [publishing events via ld-relay](README.md#using-ld-relay) +- Allow `EventPublisher` to be injected into the client. +- `GuzzleEventPublisher` as a synchronous, in-process alternative to publishing events via background processes. + ## [2.1.2] - 2017-04-27 ### Changed - Relaxed the requirement on `kevinrob/guzzle-cache-middleware` for the default `GuzzleFeatureRequester`. diff --git a/VERSION b/VERSION index e24c24656..ccbccc3dc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.2.0-beta1 +2.2.0 diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 75b98ee7a..becd4e6a6 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -11,7 +11,7 @@ class LDClient { const DEFAULT_BASE_URI = 'https://app.launchdarkly.com'; const DEFAULT_EVENTS_URI = 'https://events.launchdarkly.com'; - const VERSION = '2.2.0-beta1'; + const VERSION = '2.2.0'; /** @var string */ protected $_sdkKey;