Skip to content

Commit c87b2f5

Browse files
authored
Merge pull request #4 from launchdarkly/arun/relay-events
[CH3370] Support publishing events via ld-relay
2 parents 4e7022a + 384a406 commit c87b2f5

File tree

10 files changed

+227
-73
lines changed

10 files changed

+227
-73
lines changed

README.md

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,23 +58,40 @@ Require Guzzle as a dependency:
5858

5959
It will then be used as the default way of fetching flags.
6060

61-
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).
61+
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).
6262

6363
$client = new LaunchDarkly\LDClient("YOUR_SDK_KEY", array("cache" => $cacheStorage));
6464

6565

6666
Using LD-Relay
6767
==============
6868

69-
* 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
69+
The LaunchDarkly Relay Proxy ([ld-relay](https://github.com/launchdarkly/ld-relay)) consumes the LaunchDarkly streaming API and can update
70+
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.
7071

71-
* Require Predis as a dependency:
72+
1. Set up ld-relay in [daemon-mode](https://github.com/launchdarkly/ld-relay#redis-storage-and-daemon-mode) with Redis
7273

73-
php composer.phar require "predis/predis:1.0.*"
74+
2. Require Predis as a dependency:
7475

75-
* Create the LDClient with the Redis feature requester as an option:
76+
php composer.phar require "predis/predis:1.0.*"
7677

77-
$client = new LaunchDarkly\LDClient("your_sdk_key", ['feature_requester_class' => 'LaunchDarkly\LDDFeatureRequester', 'redis_host' => 'your.redis.host', 'redis_port' => 6379]);
78+
3. Create the LDClient with the Redis feature requester as an option:
79+
80+
$client = new LaunchDarkly\LDClient("your_sdk_key", [
81+
'feature_requester_class' => 'LaunchDarkly\LDDFeatureRequester',
82+
'redis_host' => 'your.redis.host',
83+
'redis_port' => 6379
84+
]);
85+
86+
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.
87+
88+
$client = new LaunchDarkly\LDClient("your_sdk_key", [
89+
'event_publisher_class' => 'LaunchDarkly\GuzzleEventPublisher',
90+
'events_uri' => 'http://your-ldrelay-host:8030',
91+
'feature_requester_class' => 'LaunchDarkly\LDDFeatureRequester',
92+
'redis_host' => 'your.redis.host',
93+
'redis_port' => 6379
94+
]);
7895

7996
Testing
8097
-------

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"zendframework/zend-serializer": "^2.7"
2828
},
2929
"suggest": {
30-
"guzzlehttp/guzzle": "(^6.2.1) Required when using the default FeatureRequester",
30+
"guzzlehttp/guzzle": "(^6.2.1) Required when using GuzzleEventPublisher or the default FeatureRequester",
3131
"kevinrob/guzzle-cache-middleware": "(^1.4.1) Recommended for performance when using the default FeatureRequester",
3232
"predis/predis": "(^1.0) Required when using LDDFeatureRequester"
3333
},

src/LaunchDarkly/ApcLDDFeatureRequester.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
class ApcLDDFeatureRequester extends LDDFeatureRequester {
1414
protected $_expiration = 30;
1515

16-
function __construct($baseUri, $apiKey, $options) {
17-
parent::__construct($baseUri, $apiKey, $options);
16+
function __construct($baseUri, $sdkKey, $options) {
17+
parent::__construct($baseUri, $sdkKey, $options);
1818

1919
if (isset($options['apc_expiration'])) {
2020
$this->_expiration = (int)$options['apc_expiration'];
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
namespace LaunchDarkly;
3+
4+
/**
5+
* Sends events to the LaunchDarkly service using the `curl` command line tool.
6+
* The `curl` requests are executed as background processes in order to
7+
* minimize overhead to the PHP request. This `EventPublisher` implementation
8+
* is the default for `LDClient`.
9+
*
10+
* `curl` must be installed in the environment's search path, or otherwise the
11+
* absolute path to the executable must be specified using the `'curl'` option
12+
* for `LDClient`.
13+
*/
14+
class CurlEventPublisher implements EventPublisher
15+
{
16+
private $_sdkKey;
17+
private $_host;
18+
private $_port;
19+
private $_ssl;
20+
private $_curl = '/usr/bin/env curl';
21+
22+
function __construct($sdkKey, array $options = array()) {
23+
$this->_sdkKey = $sdkKey;
24+
25+
$eventsUri = LDClient::DEFAULT_EVENTS_URI;
26+
if (isset($options['events_uri'])) {
27+
$eventsUri = $options['events_uri'];
28+
}
29+
$url = parse_url(rtrim($eventsUri,'/'));
30+
$this->_host = $url['host'];
31+
$this->_ssl = $url['scheme'] === 'https';
32+
if (isset($url['port'])) {
33+
$this->_port = $url['port'];
34+
}
35+
else {
36+
$this->_port = $this->_ssl ? 443 : 80;
37+
}
38+
if (isset($url['path'])) {
39+
$this->_path = $url['path'];
40+
}
41+
else {
42+
$this->_path = '';
43+
}
44+
45+
if (array_key_exists('curl', $options)) {
46+
$this->_curl = $options['curl'];
47+
}
48+
}
49+
50+
public function publish($payload) {
51+
$args = $this->createArgs($payload);
52+
53+
return $this->makeRequest($args);
54+
}
55+
56+
private function createArgs($payload) {
57+
$scheme = $this->_ssl ? "https://" : "http://";
58+
$args = " -X POST";
59+
$args.= " -H 'Content-Type: application/json'";
60+
$args.= " -H " . escapeshellarg("Authorization: " . $this->_sdkKey);
61+
$args.= " -H 'User-Agent: PHPClient/" . LDClient::VERSION . "'";
62+
$args.= " -H 'Accept: application/json'";
63+
$args.= " -d " . escapeshellarg($payload);
64+
$args.= " " . escapeshellarg($scheme . $this->_host . ":" . $this->_port . $this->_path . "/bulk");
65+
return $args;
66+
}
67+
68+
private function makeRequest($args) {
69+
$cmd = $this->_curl . " " . $args . ">> /dev/null 2>&1 &";
70+
shell_exec($cmd);
71+
return true;
72+
}
73+
}

src/LaunchDarkly/EventProcessor.php

Lines changed: 25 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -6,44 +6,13 @@
66
*/
77
class EventProcessor {
88

9-
private $_sdkKey;
9+
private $_eventPublisher;
1010
private $_queue;
1111
private $_capacity;
1212
private $_timeout;
13-
private $_host;
14-
private $_port;
15-
private $_ssl;
16-
private $_curl = '/usr/bin/env curl';
17-
18-
public function __construct($apiKey, $options = array()) {
19-
$this->_sdkKey = $apiKey;
20-
if (!isset($options['events_uri'])) {
21-
$this->_host = 'events.launchdarkly.com';
22-
$this->_port = 443;
23-
$this->_ssl = true;
24-
$this->_path = '';
25-
}
26-
else {
27-
$url = parse_url(rtrim($options['events_uri'],'/'));
28-
$this->_host = $url['host'];
29-
$this->_ssl = $url['scheme'] === 'https';
30-
if (isset($url['port'])) {
31-
$this->_port = $url['port'];
32-
}
33-
else {
34-
$this->_port = $this->_ssl ? 443 : 80;
35-
}
36-
if (isset($url['path'])) {
37-
$this->_path = $url['path'];
38-
}
39-
else {
40-
$this->_path = '';
41-
}
42-
}
43-
44-
if (array_key_exists('curl', $options)) {
45-
$this->_curl = $options['curl'];
46-
}
13+
14+
public function __construct($sdkKey, $options = array()) {
15+
$this->_eventPublisher = $this->getEventPublisher($sdkKey, $options);
4716

4817
$this->_capacity = $options['capacity'];
4918
$this->_timeout = $options['timeout'];
@@ -76,28 +45,29 @@ protected function flush() {
7645

7746
$payload = json_encode($this->_queue);
7847

79-
$args = $this->createArgs($payload);
80-
81-
return $this->makeRequest($args);
82-
}
83-
84-
private function createArgs($payload) {
85-
$scheme = $this->_ssl ? "https://" : "http://";
86-
$args = " -X POST";
87-
$args.= " -H 'Content-Type: application/json'";
88-
$args.= " -H " . escapeshellarg("Authorization: " . $this->_sdkKey);
89-
$args.= " -H 'User-Agent: PHPClient/" . LDClient::VERSION . "'";
90-
$args.= " -H 'Accept: application/json'";
91-
$args.= " -d " . escapeshellarg($payload);
92-
$args.= " " . escapeshellarg($scheme . $this->_host . ":" . $this->_port . $this->_path . "/bulk");
93-
return $args;
48+
return $this->_eventPublisher->publish($payload);
9449
}
9550

96-
private function makeRequest($args) {
97-
$cmd = $this->_curl . " " . $args . ">> /dev/null 2>&1 &";
98-
shell_exec($cmd);
99-
return true;
100-
}
51+
/**
52+
* @param string $sdkKey
53+
* @param mixed[] $options
54+
* @return EventPublisher
55+
*/
56+
private function getEventPublisher($sdkKey, array $options)
57+
{
58+
if (isset($options['event_publisher']) && $options['event_publisher'] instanceof EventPublisher) {
59+
return $options['event_publisher'];
60+
}
10161

62+
if (isset($options['event_publisher_class'])) {
63+
$eventPublisherClass = $options['event_publisher_class'];
64+
} else {
65+
$eventPublisherClass = CurlEventPublisher::class;
66+
}
10267

68+
if (!is_a($eventPublisherClass, EventPublisher::class, true)) {
69+
throw new \InvalidArgumentException;
70+
}
71+
return new $eventPublisherClass($sdkKey, $options);
72+
}
10373
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
namespace LaunchDarkly;
3+
4+
/**
5+
* Provides a transport mechanism for sending events to the LaunchDarkly service.
6+
*/
7+
interface EventPublisher {
8+
/**
9+
* @param string $sdkKey The SDK key for your account
10+
* @param mixed[] $options Client configuration settings
11+
*/
12+
public function __construct($sdkKey, array $options);
13+
14+
/**
15+
* Publish events to LaunchDarkly
16+
*
17+
* @param $payload string Event payload
18+
* @return bool Whether the events were successfully published
19+
*/
20+
public function publish($payload);
21+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
namespace LaunchDarkly;
3+
4+
use GuzzleHttp\Client;
5+
use Psr\Log\LoggerInterface;
6+
7+
/**
8+
* Sends events to the LaunchDarkly service using the GuzzleHttp client.
9+
* This `EventPublisher` implement provides an in-process alternative to
10+
* the default `CurlEventPublisher` implementation which forks processes.
11+
*
12+
* Note that this implementation executes synchronously in the request
13+
* handler. In order to minimize request overhead, we recommend that you
14+
* set up `ld-relay` in your production environment and configure the
15+
* `events_uri` option for `LDClient` to publish to `ld-relay`.
16+
*/
17+
class GuzzleEventPublisher implements EventPublisher
18+
{
19+
/** @var string */
20+
private $_sdkKey;
21+
/** @var string */
22+
private $_eventsUri;
23+
/** @var LoggerInterface */
24+
private $_logger;
25+
/** @var mixed[] */
26+
private $_requestOptions;
27+
28+
function __construct($sdkKey, array $options = array()) {
29+
$this->_sdkKey = $sdkKey;
30+
$this->_logger = $options['logger'];
31+
if (isset($options['events_uri'])) {
32+
$this->_eventsUri = $options['events_uri'];
33+
} else {
34+
$this->_eventsUri = LDClient::DEFAULT_EVENTS_URI;
35+
}
36+
$this->_requestOptions = [
37+
'headers' => [
38+
'Content-Type' => 'application/json',
39+
'Authorization' => $this->_sdkKey,
40+
'User-Agent' => 'PHPClient/' . LDClient::VERSION,
41+
'Accept' => 'application/json'
42+
],
43+
'timeout' => $options['timeout'],
44+
'connect_timeout' => $options['connect_timeout']
45+
];
46+
}
47+
48+
public function publish($payload) {
49+
$client = new Client(['base_uri' => $this->_eventsUri]);
50+
51+
try {
52+
$options = $this->_requestOptions;
53+
$options['body'] = $payload;
54+
$response = $client->request('POST', '/bulk', $options);
55+
56+
return $response->getStatusCode() < 300;
57+
} catch (\Exception $e) {
58+
$this->_logger->warning("GuzzleEventPublisher::publish caught $e");
59+
return false;
60+
}
61+
}
62+
}

src/LaunchDarkly/LDClient.php

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@
1010
*/
1111
class LDClient {
1212
const DEFAULT_BASE_URI = 'https://app.launchdarkly.com';
13+
const DEFAULT_EVENTS_URI = 'https://events.launchdarkly.com';
1314
const VERSION = '2.1.2';
1415

1516
/** @var string */
1617
protected $_sdkKey;
1718
/** @var string */
1819
protected $_baseUri;
20+
/** @var string */
21+
protected $_eventsUri;
1922
/** @var EventProcessor */
2023
protected $_eventProcessor;
2124
/** @var bool */
@@ -26,8 +29,7 @@ class LDClient {
2629
protected $_defaults = array();
2730
/** @var LoggerInterface */
2831
protected $_logger;
29-
30-
/** @var FeatureRequester */
32+
/** @var FeatureRequester */
3133
protected $_featureRequester;
3234

3335
/**
@@ -43,6 +45,10 @@ class LDClient {
4345
* - send_events: An optional bool that can disable the sending of events to LaunchDarkly. Defaults to false.
4446
* - logger: An optional Psr\Log\LoggerInterface. Defaults to a Monolog\Logger sending all messages to the php error_log.
4547
* - offline: An optional boolean which will disable all network calls and always return the default value. Defaults to false.
48+
* - feature_requester: An optional LaunchDarkly\FeatureRequester instance.
49+
* - feature_requester_class: An optional class implementing LaunchDarkly\FeatureRequester, if `feature_requester` is not specified. Defaults to GuzzleFeatureRequester.
50+
* - event_publisher: An optional LaunchDarkly\EventPublisher instance.
51+
* - event_publisher_class: An optional class implementing LaunchDarkly\EventPublisher, if `event_publisher` is not specified. Defaults to CurlEventPublisher.
4652
*/
4753
public function __construct($sdkKey, $options = array()) {
4854
$this->_sdkKey = $sdkKey;
@@ -51,6 +57,11 @@ public function __construct($sdkKey, $options = array()) {
5157
} else {
5258
$this->_baseUri = rtrim($options['base_uri'], '/');
5359
}
60+
if (!isset($options['events_uri'])) {
61+
$this->_eventsUri = self::DEFAULT_EVENTS_URI;
62+
} else {
63+
$this->_eventsUri = rtrim($options['events_uri'], '/');
64+
}
5465
if (isset($options['send_events'])) {
5566
$this->_send_events = $options['send_events'];
5667
}
@@ -82,15 +93,15 @@ public function __construct($sdkKey, $options = array()) {
8293

8394
$this->_eventProcessor = new EventProcessor($sdkKey, $options);
8495

85-
$this->_featureRequester = $this->getFeatureRequester($options, $sdkKey);
96+
$this->_featureRequester = $this->getFeatureRequester($sdkKey, $options);
8697
}
8798

8899
/**
89-
* @param mixed[] $options
90100
* @param string $sdkKey
101+
* @param mixed[] $options
91102
* @return FeatureRequester
92103
*/
93-
private function getFeatureRequester(array $options, $sdkKey)
104+
private function getFeatureRequester($sdkKey, array $options)
94105
{
95106
if (isset($options['feature_requester']) && $options['feature_requester'] instanceof FeatureRequester) {
96107
return $options['feature_requester'];

0 commit comments

Comments
 (0)