Skip to content

Commit

Permalink
stats: add segmentation of stats via virtual clusters (#771)
Browse files Browse the repository at this point in the history
Description: Virtual Clusters allow Envoy Mobile to emit stats based on matching criteria. This allows users to single out important traffic for their application. This PR adds an API to the client builder to add virtual cluster configuration and emission of virtual cluster stats.

Note: the client builder API exposed here is not ideal #770 tracks enhancing this.
Risk Level: low
Testing: unit tests, and local stats test. #769 tracks adding CI tests for stats
Docs Changes: added docs for the new API.

Signed-off-by: Jose Nino <jnino@lyft.com>
  • Loading branch information
junr03 authored Mar 26, 2020
1 parent 5ee55fe commit 16f5fe7
Show file tree
Hide file tree
Showing 11 changed files with 129 additions and 12 deletions.
21 changes: 21 additions & 0 deletions docs/root/api/starting_envoy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,27 @@ This information is sent as metadata when flushing stats.
// Swift
builder.addAppId("com.mydomain.myapp)

~~~~~~~~~~~~~~~~~~~~~~
``addVirtualClusters``
~~~~~~~~~~~~~~~~~~~~~~

Specify the virtual clusters config for Envoy Mobile's configuration.
The configuration is expected as a JSON list.
This functionality is used for stat segmentation.

.. attention::

This API is non-ideal as it exposes lower-level internals of Envoy than desired by this project.
:issue:`#770 <770>` tracks enhancing this API.

**Example**::

// Kotlin
builder.addVirtualClusters("[{\"name\":\"vcluster\",\"headers\":[{\"name\":\":path\",\"exact_match\":\"/v1/vcluster\"}]}]")

// Swift
builder.addVirtualClusters("[{\"name\":\"vcluster\",\"headers\":[{\"name\":\":path\",\"exact_match\":\"/v1/vcluster\"}]}]")

----------------------
Advanced configuration
----------------------
Expand Down
28 changes: 28 additions & 0 deletions library/common/config_template.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const char* config_template = R"(
virtual_hosts:
- name: api
include_attempt_count_in_response: true
virtual_clusters: {{ virtual_clusters }}
domains:
- "*"
routes:
Expand Down Expand Up @@ -157,6 +158,9 @@ stats_flush_interval: {{ stats_flush_interval_seconds }}s
- safe_regex:
google_re2: {}
regex: 'cluster\.[\w]+?\.upstream_rq_retry'
- safe_regex:
google_re2: {}
regex: 'cluster\.[\w]+?\.upstream_rq_retry_limit_exceeded'
- safe_regex:
google_re2: {}
regex: 'cluster\.[\w]+?\.upstream_rq_retry_overflow'
Expand All @@ -172,6 +176,30 @@ stats_flush_interval: {{ stats_flush_interval_seconds }}s
- safe_regex:
google_re2: {}
regex: 'cluster\.[\w]+?\.upstream_rq_unknown'
- safe_regex:
google_re2: {}
regex: 'vhost.api.vcluster\.[\w]+?\.upstream_rq_[1|2|3|4|5]xx'
- safe_regex:
google_re2: {}
regex: 'vhost.api.vcluster\.[\w]+?\.upstream_rq_retry'
- safe_regex:
google_re2: {}
regex: 'vhost.api.vcluster\.[\w]+?\.upstream_rq_retry_limit_exceeded'
- safe_regex:
google_re2: {}
regex: 'vhost.api.vcluster\.[\w]+?\.upstream_rq_retry_overflow'
- safe_regex:
google_re2: {}
regex: 'vhost.api.vcluster\.[\w]+?\.upstream_rq_retry_success'
- safe_regex:
google_re2: {}
regex: 'vhost.api.vcluster\.[\w]+?\.upstream_rq_time'
- safe_regex:
google_re2: {}
regex: 'vhost.api.vcluster\.[\w]+?\.upstream_rq_timeout'
- safe_regex:
google_re2: {}
regex: 'vhost.api.vcluster\.[\w]+?\.upstream_rq_total'
watchdog:
megamiss_timeout: 60s
miss_timeout: 60s
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class EnvoyConfiguration {
public final Integer statsFlushSeconds;
public final String appVersion;
public final String appId;
public final String virtualClusters;

/**
* Create a new instance of the configuration.
Expand All @@ -23,10 +24,12 @@ public class EnvoyConfiguration {
* @param statsFlushSeconds interval at which to flush Envoy stats.
* @param appVersion the App Version of the App using this Envoy Client.
* @param appId the App ID of the App using this Envoy Client.
* @param virtualClusters the JSON list of virtual cluster configs.
*/
public EnvoyConfiguration(String statsDomain, int connectTimeoutSeconds, int dnsRefreshSeconds,
int dnsFailureRefreshSecondsBase, int dnsFailureRefreshSecondsMax,
int statsFlushSeconds, String appVersion, String appId) {
int statsFlushSeconds, String appVersion, String appId,
String virtualClusters) {
this.statsDomain = statsDomain;
this.connectTimeoutSeconds = connectTimeoutSeconds;
this.dnsRefreshSeconds = dnsRefreshSeconds;
Expand All @@ -35,6 +38,7 @@ public EnvoyConfiguration(String statsDomain, int connectTimeoutSeconds, int dns
this.statsFlushSeconds = statsFlushSeconds;
this.appVersion = appVersion;
this.appId = appId;
this.virtualClusters = virtualClusters;
}

/**
Expand All @@ -58,7 +62,8 @@ String resolveTemplate(String templateYAML) {
.replace("{{ stats_flush_interval_seconds }}", String.format("%s", statsFlushSeconds))
.replace("{{ device_os }}", "Android")
.replace("{{ app_version }}", appVersion)
.replace("{{ app_id }}", appId);
.replace("{{ app_id }}", appId)
.replace("{{ virtual_clusters }}", virtualClusters);

if (resolvedConfiguration.contains("{{")) {
throw new ConfigurationException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ mock_template:
os: {{ device_os }}
app_version: {{ app_version }}
app_id: {{ app_id }}
virtual_clusters: {{ virtual_clusters }}
"""


class EnvoyConfigurationTest {

@Test
fun `resolving with default configuration resolves with values`() {
val envoyConfiguration = EnvoyConfiguration("stats.foo.com", 123, 234, 345, 456, 567, "v1.2.3", "com.mydomain.myapp")
val envoyConfiguration = EnvoyConfiguration("stats.foo.com", 123, 234, 345, 456, 567, "v1.2.3", "com.mydomain.myapp", "[test]");

val resolvedTemplate = envoyConfiguration.resolveTemplate(TEST_CONFIG)
assertThat(resolvedTemplate).contains("stats_domain: stats.foo.com")
Expand All @@ -35,12 +36,13 @@ class EnvoyConfigurationTest {
assertThat(resolvedTemplate).contains("os: Android")
assertThat(resolvedTemplate).contains("app_version: v1.2.3")
assertThat(resolvedTemplate).contains("app_id: com.mydomain.myapp")
assertThat(resolvedTemplate).contains("virtual_clusters: [test]")
}


@Test(expected = EnvoyConfiguration.ConfigurationException::class)
fun `resolve templates with invalid templates will throw on build`() {
val envoyConfiguration = EnvoyConfiguration("stats.foo.com", 123, 234, 345, 456, 567, "v1.2.3", "com.mydomain.myapp")
val envoyConfiguration = EnvoyConfiguration("stats.foo.com", 123, 234, 345, 456, 567, "v1.2.3", "com.mydomain.myapp", "[test]")

envoyConfiguration.resolveTemplate("{{ }}")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ open class EnvoyClientBuilder(
private var statsFlushSeconds = 60
private var appVersion = "unspecified"
private var appId = "unspecified"
private var virtualClusters = "[]"

/**
* Add a log level to use with Envoy.
Expand Down Expand Up @@ -122,6 +123,18 @@ open class EnvoyClientBuilder(
return this
}

/**
* Add virtual cluster configuration.
*
* @param virtualClusters the JSON configuration string for virtual clusters.
*
* @return this builder.
*/
fun addVirtualClusters(virtualClusters: String): EnvoyClientBuilder {
this.virtualClusters = virtualClusters
return this
}

/**
* Builds a new instance of Envoy using the provided configurations.
*
Expand All @@ -133,7 +146,7 @@ open class EnvoyClientBuilder(
return Envoy(engineType(), configuration.yaml, logLevel)
}
is Standard -> {
Envoy(engineType(), EnvoyConfiguration(statsDomain, connectTimeoutSeconds, dnsRefreshSeconds, dnsFailureRefreshSecondsBase, dnsFailureRefreshSecondsMax, statsFlushSeconds, appVersion, appId), logLevel)
Envoy(engineType(), EnvoyConfiguration(statsDomain, connectTimeoutSeconds, dnsRefreshSeconds, dnsFailureRefreshSecondsBase, dnsFailureRefreshSecondsMax, statsFlushSeconds, appVersion, appId, virtualClusters), logLevel)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,15 @@ class EnvoyBuilderTest {
val envoy = clientBuilder.build()
assertThat(envoy.envoyConfiguration!!.appId).isEqualTo("com.mydomain.myapp")
}

@Test
fun `specifying virtual clusters overrides default`() {
clientBuilder = EnvoyClientBuilder(Standard())
clientBuilder.addEngineType { engine }

clientBuilder.addVirtualClusters("[test]")
clientBuilder.build()
val envoy = clientBuilder.build()
assertThat(envoy.envoyConfiguration!!.virtualClusters).isEqualTo("[test]")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class EnvoyClientTest {

private val engine = mock(EnvoyEngine::class.java)
private val stream = mock(EnvoyHTTPStream::class.java)
private val config = EnvoyConfiguration("stats.foo.com", 0, 0, 0, 0, 0, "v1.2.3", "com.mydomain.myapp")
private val config = EnvoyConfiguration("stats.foo.com", 0, 0, 0, 0, 0, "v1.2.3", "com.mydomain.myapp", "[test]")

@Test
fun `starting a stream on envoy sends headers`() {
Expand Down
7 changes: 5 additions & 2 deletions library/objective-c/EnvoyConfiguration.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ - (instancetype)initWithStatsDomain:(NSString *)statsDomain
dnsFailureRefreshSecondsMax:(UInt32)dnsFailureRefreshSecondsMax
statsFlushSeconds:(UInt32)statsFlushSeconds
appVersion:(NSString *)appVersion
appId:(NSString *)appId {
appId:(NSString *)appId
virtualClusters:(NSString *)virtualClusters {
self = [super init];
if (!self) {
return nil;
Expand All @@ -25,6 +26,7 @@ - (instancetype)initWithStatsDomain:(NSString *)statsDomain
self.statsFlushSeconds = statsFlushSeconds;
self.appVersion = appVersion;
self.appId = appId;
self.virtualClusters = virtualClusters;
return self;
}

Expand All @@ -43,7 +45,8 @@ - (nullable NSString *)resolveTemplate:(NSString *)templateYAML {
[NSString stringWithFormat:@"%lu", (unsigned long)self.statsFlushSeconds],
@"device_os" : @"iOS",
@"app_version" : self.appVersion,
@"app_id" : self.appId
@"app_id" : self.appId,
@"virtual_clusters" : self.virtualClusters
};

for (NSString *templateKey in templateKeysToValues) {
Expand Down
4 changes: 3 additions & 1 deletion library/objective-c/EnvoyEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ typedef NSDictionary<NSString *, NSArray<NSString *> *> EnvoyHeaders;
@property (nonatomic, assign) UInt32 statsFlushSeconds;
@property (nonatomic, strong) NSString *appVersion;
@property (nonatomic, strong) NSString *appId;
@property (nonatomic, strong) NSString *virtualClusters;

/**
Create a new instance of the configuration.
Expand All @@ -149,7 +150,8 @@ typedef NSDictionary<NSString *, NSArray<NSString *> *> EnvoyHeaders;
dnsFailureRefreshSecondsMax:(UInt32)dnsFailureRefreshSecondsMax
statsFlushSeconds:(UInt32)statsFlushSeconds
appVersion:(NSString *)appVersion
appId:(NSString *)appId;
appId:(NSString *)appId
virtualClusters:(NSString *)virtualClusters;

/**
Resolves the provided configuration template using properties on this configuration.
Expand Down
16 changes: 15 additions & 1 deletion library/swift/src/EnvoyClientBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public final class EnvoyClientBuilder: NSObject {
private var statsFlushSeconds: UInt32 = 60
private var appVersion: String = "unspecified"
private var appId: String = "unspecified"
private var virtualClusters: String = "[]"

// MARK: - Public

Expand Down Expand Up @@ -127,6 +128,18 @@ public final class EnvoyClientBuilder: NSObject {
return self
}

///
/// Add virtual cluster configuration.
///
/// - paramenter virtualClusters: The JSON configuration string for virtual clusters.
///
/// returns: This builder.
@discardableResult
public func addVirtualClusters(_ virtualClusters: String) -> EnvoyClientBuilder {
self.virtualClusters = virtualClusters
return self
}

/// Builds a new instance of EnvoyClient using the provided configurations.
///
/// - returns: A new instance of EnvoyClient.
Expand All @@ -144,7 +157,8 @@ public final class EnvoyClientBuilder: NSObject {
dnsFailureRefreshSecondsMax: self.dnsFailureRefreshSecondsMax,
statsFlushSeconds: self.statsFlushSeconds,
appVersion: self.appVersion,
appId: self.appId)
appId: self.appId,
virtualClusters: self.virtualClusters)
return EnvoyClient(config: config, logLevel: self.logLevel, engine: engine)
}
}
Expand Down
22 changes: 20 additions & 2 deletions library/swift/test/EnvoyClientBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ mock_template:
stats_flush_interval: {{ stats_flush_interval_seconds }}s
app_version: {{ app_version }}
app_id: {{ app_id }}
virtual_clusters: {{ virtual_clusters }}
"""

private final class MockEnvoyEngine: NSObject, EnvoyEngine {
Expand Down Expand Up @@ -169,6 +170,20 @@ final class EnvoyClientBuilderTests: XCTestCase {
self.waitForExpectations(timeout: 0.01)
}

func testAddingVirtualClustersAddsToConfigurationWhenRunningEnvoy() throws {
let expectation = self.expectation(description: "Run called with expected data")
MockEnvoyEngine.onRunWithConfig = { config, _ in
XCTAssertEqual("[test]", config.virtualClusters)
expectation.fulfill()
}

_ = try EnvoyClientBuilder()
.addEngineType(MockEnvoyEngine.self)
.addVirtualClusters("[test]")
.build()
self.waitForExpectations(timeout: 0.01)
}

func testResolvesYAMLWithIndividuallySetValues() throws {
let config = EnvoyConfiguration(statsDomain: "stats.foo.com",
connectTimeoutSeconds: 200,
Expand All @@ -177,7 +192,8 @@ final class EnvoyClientBuilderTests: XCTestCase {
dnsFailureRefreshSecondsMax: 500,
statsFlushSeconds: 600,
appVersion: "v1.2.3",
appId: "com.mydomain.myapp")
appId: "com.mydomain.myapp",
virtualClusters: "[test]")
guard let resolvedYAML = config.resolveTemplate(kMockTemplate) else {
XCTFail("Resolved template YAML is nil")
return
Expand All @@ -192,6 +208,7 @@ final class EnvoyClientBuilderTests: XCTestCase {
XCTAssertTrue(resolvedYAML.contains("device_os: iOS"))
XCTAssertTrue(resolvedYAML.contains("app_version: v1.2.3"))
XCTAssertTrue(resolvedYAML.contains("app_id: com.mydomain.myapp"))
XCTAssertTrue(resolvedYAML.contains("virtual_clusters: [test]"))
}

func testReturnsNilWhenUnresolvedValueInTemplate() {
Expand All @@ -202,7 +219,8 @@ final class EnvoyClientBuilderTests: XCTestCase {
dnsFailureRefreshSecondsMax: 500,
statsFlushSeconds: 600,
appVersion: "v1.2.3",
appId: "com.mydomain.myapp")
appId: "com.mydomain.myapp",
virtualClusters: "[test]")
XCTAssertNil(config.resolveTemplate("{{ missing }}"))
}
}

0 comments on commit 16f5fe7

Please sign in to comment.