Skip to content

Commit

Permalink
feat: server-side payload filters (#435)
Browse files Browse the repository at this point in the history
This PR introduces Payload Filters to the C++ Server-Side SDK. The
filter key can be configured individually on the Streaming or Polling
data sources.

If an invalid key is provided (empty or not matching regex), a runtime
error will be logged at the time of data source construction. The SDK
will then request an unfiltered environment as a failsafe.
  • Loading branch information
cwaldren-ld authored Sep 4, 2024
1 parent 9c621cf commit aaff0b8
Show file tree
Hide file tree
Showing 14 changed files with 326 additions and 23 deletions.
29 changes: 26 additions & 3 deletions contract-tests/data-model/include/data_model/data_model.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
#include "nlohmann/json.hpp"

Check failure on line 6 in contract-tests/data-model/include/data_model/data_model.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/data-model/include/data_model/data_model.hpp:6:10 [clang-diagnostic-error]

'nlohmann/json.hpp' file not found

namespace nlohmann {

template <typename T>
struct adl_serializer<std::optional<T>> {
static void to_json(json& j, std::optional<T> const& opt) {
Expand Down Expand Up @@ -41,18 +40,24 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigTLSParams,
struct ConfigStreamingParams {
std::optional<std::string> baseUri;
std::optional<uint32_t> initialRetryDelayMs;
std::optional<std::string> filter;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigStreamingParams,
baseUri,
initialRetryDelayMs);
initialRetryDelayMs,
filter);

struct ConfigPollingParams {
std::optional<std::string> baseUri;
std::optional<uint32_t> pollIntervalMs;
std::optional<std::string> filter;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigPollingParams,
baseUri,
pollIntervalMs);
pollIntervalMs,
filter);

struct ConfigEventParams {

Check warning on line 62 in contract-tests/data-model/include/data_model/data_model.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/data-model/include/data_model/data_model.hpp:62:8 [cppcoreguidelines-pro-type-member-init]

constructor does not initialize these fields: globalPrivateAttributes
std::optional<std::string> baseUri;
Expand All @@ -62,18 +67,21 @@ struct ConfigEventParams {
std::vector<std::string> globalPrivateAttributes;
std::optional<int> flushIntervalMs;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigEventParams,
baseUri,
capacity,
enableDiagnostics,
allAttributesPrivate,
globalPrivateAttributes,
flushIntervalMs);

struct ConfigServiceEndpointsParams {
std::optional<std::string> streaming;
std::optional<std::string> polling;
std::optional<std::string> events;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigServiceEndpointsParams,
streaming,
polling,
Expand All @@ -84,6 +92,7 @@ struct ConfigClientSideParams {
std::optional<bool> evaluationReasons;
std::optional<bool> useReport;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigClientSideParams,
initialContext,
evaluationReasons,
Expand All @@ -93,6 +102,7 @@ struct ConfigTags {
std::optional<std::string> applicationId;
std::optional<std::string> applicationVersion;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigTags,
applicationId,
applicationVersion);
Expand All @@ -109,6 +119,7 @@ struct ConfigParams {
std::optional<ConfigTags> tags;
std::optional<ConfigTLSParams> tls;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigParams,
credential,
startWaitTimeMs,
Expand Down Expand Up @@ -141,6 +152,7 @@ inline void to_json(nlohmann::json& nlohmann_json_j,
nlohmann_json_j["private"] = nlohmann_json_t._private;
nlohmann_json_j["custom"] = nlohmann_json_t.custom;
}

inline void from_json(nlohmann::json const& nlohmann_json_j,
ContextSingleParams& nlohmann_json_t) {
ContextSingleParams nlohmann_json_default_obj;
Expand Down Expand Up @@ -184,11 +196,13 @@ struct CreateInstanceParams {
ConfigParams configuration;
std::string tag;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(CreateInstanceParams,
configuration,
tag);

enum class ValueType { Bool = 1, Int, Double, String, Any, Unspecified };

NLOHMANN_JSON_SERIALIZE_ENUM(ValueType,
{{ValueType::Bool, "bool"},
{ValueType::Int, "int"},
Expand All @@ -205,6 +219,7 @@ struct EvaluateFlagParams {
bool detail;
EvaluateFlagParams();
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EvaluateFlagParams,
flagKey,
context,
Expand All @@ -217,6 +232,7 @@ struct EvaluateFlagResponse {
std::optional<uint32_t> variationIndex;
std::optional<nlohmann::json> reason;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EvaluateFlagResponse,
value,
variationIndex,
Expand All @@ -228,14 +244,17 @@ struct EvaluateAllFlagParams {
std::optional<bool> clientSideOnly;
std::optional<bool> detailsOnlyForTrackedFlags;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EvaluateAllFlagParams,
context,
withReasons,
clientSideOnly,
detailsOnlyForTrackedFlags);

struct EvaluateAllFlagsResponse {

Check warning on line 254 in contract-tests/data-model/include/data_model/data_model.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/data-model/include/data_model/data_model.hpp:254:8 [cppcoreguidelines-pro-type-member-init]

constructor does not initialize these fields: state
nlohmann::json state;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EvaluateAllFlagsResponse,
state);

Expand All @@ -246,6 +265,7 @@ struct CustomEventParams {
std::optional<bool> omitNullData;
std::optional<double> metricValue;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(CustomEventParams,
eventKey,
context,
Expand All @@ -256,6 +276,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(CustomEventParams,
struct IdentifyEventParams {

Check warning on line 276 in contract-tests/data-model/include/data_model/data_model.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/data-model/include/data_model/data_model.hpp:276:8 [cppcoreguidelines-pro-type-member-init]

constructor does not initialize these fields: context
nlohmann::json context;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(IdentifyEventParams, context);

enum class Command {
Expand All @@ -268,6 +289,7 @@ enum class Command {
ContextBuild,
ContextConvert
};

NLOHMANN_JSON_SERIALIZE_ENUM(Command,
{{Command::Unknown, nullptr},
{Command::EvaluateFlag, "evaluate"},
Expand All @@ -288,6 +310,7 @@ struct CommandParams {
std::optional<ContextConvertParams> contextConvert;
CommandParams();
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(CommandParams,
command,
evaluate,
Expand Down
10 changes: 8 additions & 2 deletions contract-tests/server-contract-tests/src/entity_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,15 @@ std::optional<std::string> EntityManager::create(ConfigParams const& in) {
if (in.streaming->baseUri) {
endpoints.StreamingBaseUrl(*in.streaming->baseUri);
}
auto streaming = decltype(datasystem)::Streaming();
if (in.streaming->initialRetryDelayMs) {
auto streaming = decltype(datasystem)::Streaming();
streaming.InitialReconnectDelay(
std::chrono::milliseconds(*in.streaming->initialRetryDelayMs));
datasystem.Synchronizer(std::move(streaming));
}
if (in.streaming->filter) {
streaming.Filter(*in.streaming->filter);
}
datasystem.Synchronizer(std::move(streaming));
}

if (in.polling) {
Expand All @@ -72,6 +75,9 @@ std::optional<std::string> EntityManager::create(ConfigParams const& in) {
std::chrono::milliseconds(
*in.polling->pollIntervalMs)));
}
if (in.polling->filter) {
method.Filter(*in.polling->filter);
}
datasystem.Synchronizer(std::move(method));
}
}
Expand Down
7 changes: 4 additions & 3 deletions contract-tests/server-contract-tests/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ int main(int argc, char* argv[]) {
try {
net::io_context ioc{1};

auto p = boost::lexical_cast<unsigned short>(port);
server srv(ioc, "0.0.0.0", p, logger);
auto const p = boost::lexical_cast<unsigned short>(port);

Check warning on line 34 in contract-tests/server-contract-tests/src/main.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/server-contract-tests/src/main.cpp:34:20 [readability-identifier-length]

variable name 'p' is too short, expected at least 3 characters
server srv{ioc, "0.0.0.0", p, logger};

Check warning on line 35 in contract-tests/server-contract-tests/src/main.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/server-contract-tests/src/main.cpp:35:16 [cppcoreguidelines-init-variables]

variable 'srv' is not initialized

srv.add_capability("server-side");
srv.add_capability("strongly-typed");
Expand All @@ -45,7 +45,8 @@ int main(int argc, char* argv[]) {
srv.add_capability("tls:verify-peer");
srv.add_capability("tls:skip-verify-peer");
srv.add_capability("tls:custom-ca");

srv.add_capability("filtering");
srv.add_capability("filtering-strict");
net::signal_set signals{ioc, SIGINT, SIGTERM};

boost::asio::spawn(ioc.get_executor(), [&](auto yield) mutable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,24 @@
#include <launchdarkly/config/shared/defaults.hpp>
#include <launchdarkly/config/shared/sdks.hpp>

#include <launchdarkly/error.hpp>

#include <tl/expected.hpp>

#include <chrono>
#include <type_traits>
#include <variant>

namespace launchdarkly::config::shared::builders {

/**
* Used to construct a DataSourceConfiguration for the specified SDK type.
* @tparam SDK ClientSDK or ServerSDK.
*/
template <typename SDK>
class DataSourceBuilder;

template <typename T>
struct is_server_sdk : std::false_type {};

template <>
struct is_server_sdk<ServerSDK> : std::true_type {};

/**
* Builds a configuration for a streaming data source.
*/
Expand All @@ -43,6 +45,30 @@ class StreamingBuilder {
StreamingBuilder& InitialReconnectDelay(
std::chrono::milliseconds initial_reconnect_delay);

/**
* Sets the filter key for the streaming connection.
*
* By default, the SDK is able to evaluate all flags in an environment.
*
* If this is undesirable - for example, because the environment contains
* thousands of flags, but this application only needs to evaluate
* a smaller, known subset - then a filter may be setup in LaunchDarkly,
* and the filter's key specified here.
*
* Evaluations for flags that aren't part of the filtered environment will
* return default values.
* @param filter_key The filter key. If the key is malformed or nonexistent,
* then a full LaunchDarkly environment will be fetched. In the case of a
* malformed key, the SDK will additionally log a runtime error.
* @return Reference to this builder.
*/
template <typename T = SDK>
std::enable_if_t<is_server_sdk<T>::value, StreamingBuilder&> Filter(
std::string filter_key) {
config_.filter_key = std::move(filter_key);
return *this;
}

/**
* Build the streaming config. Used internal to the SDK.
* @return The built config.
Expand All @@ -68,6 +94,31 @@ class PollingBuilder {
*/
PollingBuilder& PollInterval(std::chrono::seconds poll_interval);

/**
* Sets the filter key for the polling connection.
*
* By default, the SDK is able to evaluate all flags in an environment.
*
* If this is undesirable - for example, because the environment contains
* thousands of flags, but this application only needs to evaluate
* a smaller, known subset - then a filter may be setup in LaunchDarkly,
* and the filter's key specified here.
*
* Evaluations for flags that aren't part of the filtered environment will
* return default values.
*
* @param filter_key The filter key. If the key is malformed or nonexistent,
* then a full LaunchDarkly environment will be fetched. In the case of a
* malformed key, the SDK will additionally log a runtime error.
* @return Reference to this builder.
*/
template <typename T = SDK>
std::enable_if_t<is_server_sdk<T>::value, PollingBuilder&> Filter(
std::string filter_key) {
config_.filter_key = std::move(filter_key);
return *this;
}

/**
* Build the polling config. Used internal to the SDK.
* @return The built config.
Expand Down Expand Up @@ -100,7 +151,7 @@ class DataSourceBuilder<ClientSDK> {
DataSourceBuilder& WithReasons(bool value);

/**
* Whether or not to use the REPORT verb to fetch flag settings.
* Whether to use the REPORT verb to fetch flag settings.
*
* If this is true, flag settings will be fetched with a REPORT request
* including a JSON entity body with the context object.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ template <>
struct StreamingConfig<ServerSDK> {
std::chrono::milliseconds initial_reconnect_delay;
std::string streaming_path;
std::optional<std::string> filter_key;
};

inline bool operator==(StreamingConfig<ServerSDK> const& lhs,
StreamingConfig<ServerSDK> const& rhs) {
return lhs.initial_reconnect_delay == rhs.initial_reconnect_delay &&
lhs.streaming_path == rhs.streaming_path;
lhs.streaming_path == rhs.streaming_path &&
lhs.filter_key == rhs.filter_key;
}

template <typename SDK>
Expand All @@ -46,6 +48,7 @@ struct PollingConfig<ServerSDK> {
std::chrono::seconds poll_interval;
std::string polling_get_path;
std::chrono::seconds min_polling_interval;
std::optional<std::string> filter_key;
};

template <typename SDK>
Expand Down
Loading

0 comments on commit aaff0b8

Please sign in to comment.