diff --git a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java index 1355fdd4..4152b158 100644 --- a/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java +++ b/lib/sdk/server/contract-tests/service/src/main/java/sdktest/TestService.java @@ -42,7 +42,8 @@ public class TestService { "strongly-typed", "tags", "server-side-polling", - "fdv1-fallback" + "fdv1-fallback", + "instance-id" }; static final Gson gson = new GsonBuilder().serializeNulls().create(); diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/ClientContextImpl.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/ClientContextImpl.java index 6146b39c..16e37d16 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/ClientContextImpl.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/ClientContextImpl.java @@ -8,6 +8,7 @@ import com.launchdarkly.sdk.server.subsystems.HttpConfiguration; import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration; +import java.util.UUID; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -37,7 +38,7 @@ private ClientContextImpl( ) { super(baseContext.getSdkKey(), baseContext.getApplicationInfo(), baseContext.getHttp(), baseContext.getLogging(), baseContext.isOffline(), baseContext.getServiceEndpoints(), - baseContext.getThreadPriority(), baseContext.getWrapperInfo()); + baseContext.getThreadPriority(), baseContext.getWrapperInfo(), baseContext.getInstanceId()); this.sharedExecutor = sharedExecutor; this.diagnosticStore = diagnosticStore; this.dataSourceUpdateSink = null; @@ -79,22 +80,29 @@ static ClientContextImpl fromConfig( LDConfig config, ScheduledExecutorService sharedExecutor ) { + // Generate the instance ID once and thread it through every ClientContext we build for this + // LDClient. Subsystems built from any of these contexts will all observe the same value. + String instanceId = UUID.randomUUID().toString(); + ClientContext minimalContext = new ClientContext(sdkKey, config.applicationInfo, null, - null, config.offline, config.serviceEndpoints, config.threadPriority, config.wrapperInfo); + null, config.offline, config.serviceEndpoints, config.threadPriority, config.wrapperInfo, + instanceId); LoggingConfiguration loggingConfig = config.logging.build(minimalContext); - + ClientContext contextWithLogging = new ClientContext(sdkKey, config.applicationInfo, null, - loggingConfig, config.offline, config.serviceEndpoints, config.threadPriority, config.wrapperInfo); + loggingConfig, config.offline, config.serviceEndpoints, config.threadPriority, + config.wrapperInfo, instanceId); HttpConfiguration httpConfig = config.http.build(contextWithLogging); - + if (httpConfig.getProxy() != null) { contextWithLogging.getBaseLogger().info("Using proxy: {} {} authentication.", httpConfig.getProxy(), httpConfig.getProxyAuthentication() == null ? "without" : "with"); } - - ClientContext contextWithHttpAndLogging = new ClientContext(sdkKey, config.applicationInfo, httpConfig, - loggingConfig, config.offline, config.serviceEndpoints, config.threadPriority, config.wrapperInfo); + + ClientContext contextWithHttpAndLogging = new ClientContext(sdkKey, config.applicationInfo, + httpConfig, loggingConfig, config.offline, config.serviceEndpoints, config.threadPriority, + config.wrapperInfo, instanceId); // Create a diagnostic store only if diagnostics are enabled. Diagnostics are enabled as long as 1. the // opt-out property was not set in the config, and 2. we are using the standard event processor. diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java index cf9351b9..3b62c41a 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java @@ -304,6 +304,15 @@ private Result transformResult(com.launchdarkly.sdk.server.subsystems.EventSende } static final class HttpConfigurationBuilderImpl extends HttpConfigurationBuilder { + /** + * HTTP header used to identify this SDK instance for the purpose of estimating + * server-connection-minutes when polling. It contains a v4 UUID that is generated once per SDK + * instance and remains constant for the lifetime of the client. + * + *
See: sdk-specs / SCMP-server-connection-minutes-polling. + */ + static final String INSTANCE_ID_HEADER = "X-LaunchDarkly-Instance-Id"; + @Override public HttpConfiguration build(ClientContext clientContext) { LDLogger logger = clientContext.getBaseLogger(); @@ -340,6 +349,16 @@ else if (wrapperName != null) { headers.put("X-LaunchDarkly-Wrapper", wrapperId); } + // The instance ID originates on ClientContext (generated once when LDClient is constructed) + // so every subsystem built from the same context observes a consistent value for the + // lifetime of the SDK instance. + String instanceId = clientContext.getInstanceId(); + if (instanceId != null && !instanceId.isEmpty()) { + headers.put(INSTANCE_ID_HEADER, instanceId); + } + + // For consistency with other SDKs, custom headers are allowed to overwrite headers such as + // User-Agent and Authorization. if (!customHeaders.isEmpty()) { headers.putAll(customHeaders); } diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/ClientContext.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/ClientContext.java index 1df46b34..9191d7e9 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/ClientContext.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/ClientContext.java @@ -8,6 +8,8 @@ import com.launchdarkly.sdk.server.interfaces.ServiceEndpoints; import com.launchdarkly.sdk.server.interfaces.WrapperInfo; +import java.util.UUID; + /** * Context information provided by the {@link com.launchdarkly.sdk.server.LDClient} when creating components. *
@@ -31,11 +33,18 @@ public class ClientContext { private final boolean offline; private final ServiceEndpoints serviceEndpoints; private final int threadPriority; + private final String instanceId; private WrapperInfo wrapperInfo; /** - * Constructor that sets all properties. All should be non-null. - * + * Constructor that sets all properties including an explicit instance ID. All should be + * non-null. + * + *
The instance ID is sent on every outbound request in the {@code X-LaunchDarkly-Instance-Id}
+ * header. It must be generated once per LDClient and remain stable for the client's lifetime.
+ * The eight-argument constructor auto-generates a v4 UUID for callers that do not need to
+ * supply their own value.
+ *
* @param sdkKey the SDK key
* @param applicationInfo application metadata properties from
* {@link Builder#applicationInfo(com.launchdarkly.sdk.server.integrations.ApplicationInfoBuilder)}
@@ -46,6 +55,7 @@ public class ClientContext {
* {@link Builder#serviceEndpoints(com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder)}
* @param threadPriority worker thread priority from {@link Builder#threadPriority(int)}
* @param wrapperInfo wrapper configuration from {@link Builder#wrapper(com.launchdarkly.sdk.server.integrations.WrapperInfoBuilder)}
+ * @param instanceId per-LDClient identifier for the {@code X-LaunchDarkly-Instance-Id} header
*/
public ClientContext(
String sdkKey,
@@ -55,7 +65,8 @@ public ClientContext(
boolean offline,
ServiceEndpoints serviceEndpoints,
int threadPriority,
- WrapperInfo wrapperInfo
+ WrapperInfo wrapperInfo,
+ String instanceId
) {
this.sdkKey = sdkKey;
this.applicationInfo = applicationInfo;
@@ -65,19 +76,51 @@ public ClientContext(
this.serviceEndpoints = serviceEndpoints;
this.threadPriority = threadPriority;
this.wrapperInfo = wrapperInfo;
-
+ this.instanceId = instanceId;
+
this.baseLogger = logging == null ? LDLogger.none() :
LDLogger.withAdapter(logging.getLogAdapter(), logging.getBaseLoggerName());
}
-
+
+ /**
+ * Constructor that sets all properties. All should be non-null. Auto-generates a v4 UUID for
+ * the instance ID; use the nine-argument constructor if you need to thread an existing value
+ * through (for example, when copying a context for an in-flight LDClient).
+ *
+ * @param sdkKey the SDK key
+ * @param applicationInfo application metadata properties from
+ * {@link Builder#applicationInfo(com.launchdarkly.sdk.server.integrations.ApplicationInfoBuilder)}
+ * @param http HTTP configuration properties from {@link Builder#http(ComponentConfigurer)}
+ * @param logging logging configuration properties from {@link Builder#logging(ComponentConfigurer)}
+ * @param offline true if the SDK should be entirely offline
+ * @param serviceEndpoints service endpoint URI properties from
+ * {@link Builder#serviceEndpoints(com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder)}
+ * @param threadPriority worker thread priority from {@link Builder#threadPriority(int)}
+ * @param wrapperInfo wrapper configuration from {@link Builder#wrapper(com.launchdarkly.sdk.server.integrations.WrapperInfoBuilder)}
+ */
+ public ClientContext(
+ String sdkKey,
+ ApplicationInfo applicationInfo,
+ HttpConfiguration http,
+ LoggingConfiguration logging,
+ boolean offline,
+ ServiceEndpoints serviceEndpoints,
+ int threadPriority,
+ WrapperInfo wrapperInfo
+ ) {
+ this(sdkKey, applicationInfo, http, logging, offline, serviceEndpoints, threadPriority,
+ wrapperInfo, UUID.randomUUID().toString());
+ }
+
/**
* Copy constructor.
- *
+ *
* @param copyFrom the instance to copy from
*/
protected ClientContext(ClientContext copyFrom) {
this(copyFrom.sdkKey, copyFrom.applicationInfo, copyFrom.http, copyFrom.logging,
- copyFrom.offline, copyFrom.serviceEndpoints, copyFrom.threadPriority, copyFrom.wrapperInfo);
+ copyFrom.offline, copyFrom.serviceEndpoints, copyFrom.threadPriority, copyFrom.wrapperInfo,
+ copyFrom.instanceId);
}
/**
@@ -199,13 +242,24 @@ public ServiceEndpoints getServiceEndpoints() {
/**
* Returns the worker thread priority that is set by
* {@link Builder#threadPriority(int)}.
- *
+ *
* @return the thread priority
*/
public int getThreadPriority() {
return threadPriority;
}
+ /**
+ * Returns the per-LDClient instance identifier sent in the {@code X-LaunchDarkly-Instance-Id}
+ * header on outbound requests. The value is generated once when the {@link ClientContext} is
+ * constructed and is stable for the client's lifetime.
+ *
+ * @return the instance ID
+ */
+ public String getInstanceId() {
+ return instanceId;
+ }
+
/**
* Returns the wrapper information.
*
diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/DefaultFeatureRequestorTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/DefaultFeatureRequestorTest.java
index 1b7f315f..7cc6ac9c 100644
--- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/DefaultFeatureRequestorTest.java
+++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/DefaultFeatureRequestorTest.java
@@ -245,6 +245,14 @@ public void ignoreEmptyFilter() throws Exception {
private void verifyHeaders(RequestInfo req) {
HttpConfiguration httpConfig = clientContext(sdkKey, LDConfig.DEFAULT).getHttp();
for (Map.Entry