Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1173,7 +1173,22 @@ public enum DefaultDriverOption implements DriverOption {
*
* <p>Value-Type: boolean
*/
ADDRESS_TRANSLATOR_RESOLVE_ADDRESSES("advanced.address-translator.resolve-addresses");
ADDRESS_TRANSLATOR_RESOLVE_ADDRESSES("advanced.address-translator.resolve-addresses"),
/**
* Whether the driver reports its effective configuration to ScyllaDB at connection time.
*
* <p>When {@code true}, the driver adds two entries to the CQL {@code STARTUP} options, which
* ScyllaDB stores in {@code system.clients.client_options} so operators can inspect driver
* settings while investigating incidents: a {@code SESSION_ID} on every connection (so the server
* can group a session's connections) and a compact JSON payload under the {@code DRIVER_CONFIG}
* key on the control connection only. At this stage the {@code DRIVER_CONFIG} payload carries
* only schema-version metadata (<code>{"version":1}</code>); reporting of the effective
* configuration fields is planned for a later stage. When {@code false}, neither entry is sent
* and there is no change on the wire.
*
* <p>Value type: boolean
*/
DRIVER_CONFIG_REPORTING_ENABLED("advanced.driver-config-reporting.enabled");

private final String path;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ protected static void fillWithDriverDefaults(OptionsMap map) {
// values) with no sensible scalar default, analogous to how CONFIG_RELOAD_INTERVAL is omitted.
map.put(TypedDriverOption.CLIENT_ROUTES_NATIVE_TRANSPORT_PORT, 9042);
map.put(TypedDriverOption.CLIENT_ROUTES_SHARD_AWARENESS_ENABLED, false);
map.put(TypedDriverOption.DRIVER_CONFIG_REPORTING_ENABLED, false);
}

@Immutable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,11 @@ public String toString() {
new TypedDriverOption<>(
DefaultDriverOption.CLIENT_ROUTES_SHARD_AWARENESS_ENABLED, GenericType.BOOLEAN);

/** Whether the driver reports its configuration to ScyllaDB at connection time. */
public static final TypedDriverOption<Boolean> DRIVER_CONFIG_REPORTING_ENABLED =
new TypedDriverOption<>(
DefaultDriverOption.DRIVER_CONFIG_REPORTING_ENABLED, GenericType.BOOLEAN);

private static Iterable<TypedDriverOption<?>> introspectBuiltInValues() {
try {
ImmutableList.Builder<TypedDriverOption<?>> result = ImmutableList.builder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,32 @@ public static Builder builder() {

public final String ownerLogPrefix;

/**
* Whether the channel should report the driver configuration ({@code DRIVER_CONFIG}) in its
* {@code STARTUP} options. Set only for the control connection, so the (potentially large) config
* blob is sent once per session rather than on every pooled connection.
*/
public final boolean reportConfig;

private DriverChannelOptions(
CqlIdentifier keyspace,
List<String> eventTypes,
EventCallback eventCallback,
String ownerLogPrefix) {
String ownerLogPrefix,
boolean reportConfig) {
this.keyspace = keyspace;
this.eventTypes = eventTypes;
this.eventCallback = eventCallback;
this.ownerLogPrefix = ownerLogPrefix;
this.reportConfig = reportConfig;
}

public static class Builder {
private CqlIdentifier keyspace = null;
private List<String> eventTypes = Collections.emptyList();
private EventCallback eventCallback = null;
private String ownerLogPrefix = null;
private boolean reportConfig = false;

public Builder withKeyspace(CqlIdentifier keyspace) {
this.keyspace = keyspace;
Expand All @@ -82,8 +92,17 @@ public Builder withOwnerLogPrefix(String ownerLogPrefix) {
return this;
}

/**
* Marks this channel as the one that reports {@code DRIVER_CONFIG} (the control connection).
*/
public Builder reportConfig(boolean reportConfig) {
this.reportConfig = reportConfig;
return this;
}

public DriverChannelOptions build() {
return new DriverChannelOptions(keyspace, eventTypes, eventCallback, ownerLogPrefix);
return new DriverChannelOptions(
keyspace, eventTypes, eventCallback, ownerLogPrefix, reportConfig);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,11 @@ Message getRequest() {
if (featureStore != null) {
featureStore.populateStartupOptions(startupOptions);
}
// Adds SESSION_ID on every connection and DRIVER_CONFIG on the control connection
// (options.reportConfig); no-op when driver config reporting is disabled.
context
.getDriverConfigReporter()
.populateStartupOptions(startupOptions, options.reportConfig);
return request = new Startup(startupOptions);
case GET_CLUSTER_NAME:
return request = CLUSTER_NAME_QUERY;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.datastax.oss.driver.internal.core.context;

import com.datastax.oss.driver.api.core.config.DefaultDriverOption;
import com.datastax.oss.driver.api.core.config.DriverExecutionProfile;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Map;
import java.util.UUID;
import net.jcip.annotations.ThreadSafe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Default {@link DriverConfigReporter}: serializes the driver configuration to the cross-driver
* {@code DRIVER_CONFIG} JSON shape and adds it to the control connection's {@code STARTUP} options.
*
* <p>The blob is (re)built on demand every time the control connection initializes, so it always
* reflects the current (possibly reloaded) configuration without any caching.
*/
@ThreadSafe
public class DefaultDriverConfigReporter implements DriverConfigReporter {

private static final Logger LOG = LoggerFactory.getLogger(DefaultDriverConfigReporter.class);

/** STARTUP option key under which the config JSON is sent. */
public static final String DRIVER_CONFIG_KEY = "DRIVER_CONFIG";

/** STARTUP option key under which the per-session identifier is sent. */
public static final String SESSION_ID_KEY = "SESSION_ID";

/**
* Major schema version. Adding keys is backward-compatible and does not bump this; only
* changing/removing the meaning of an existing key does.
*/
static final int SCHEMA_VERSION = 1;

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

protected final InternalDriverContext context;

// Dedicated, driver-generated identifier for this session. Not derived from the (user-settable,
// Insights-oriented) CLIENT_ID, so that it is guaranteed unique per session as the grouping key
// requires. The reporter is a per-session singleton (built once via LazyReference), so this value
// is stable and shared across all of the session's connections.
private final UUID sessionId = Uuids.random();

public DefaultDriverConfigReporter(InternalDriverContext context) {
this.context = context;
}

@Override
public void populateStartupOptions(
Map<String, String> startupOptions, boolean reportDriverConfig) {
// Configuration reporting is a best-effort diagnostic aid: it runs on the connection
// initialization path, so any failure here (a bad config read, a misbehaving policy while
// introspecting, a serialization error) must be swallowed rather than allowed to break the
// connection — which would prevent the session from establishing or reconnecting.
try {
if (!isEnabled()) {
return;
}
// SESSION_ID on every connection so the server can group a session's connections.
startupOptions.put(SESSION_ID_KEY, sessionId.toString());
// DRIVER_CONFIG blob only on the control connection.
if (reportDriverConfig) {
String json = buildJson();
if (json != null) {
startupOptions.put(DRIVER_CONFIG_KEY, json);
}
}
} catch (RuntimeException e) {
LOG.warn(
"Error while building the driver configuration report; skipping driver config reporting",
e);
}
}

private boolean isEnabled() {
return context
.getConfig()
.getDefaultProfile()
.getBoolean(DefaultDriverOption.DRIVER_CONFIG_REPORTING_ENABLED, false);
}

/**
* Builds the compact, single-line JSON configuration report.
*
* <p>Stage 1 emits only the schema {@code version}; the individual configuration groups are
* populated in {@link #populateConfig(ObjectNode, DriverExecutionProfile)} in a later stage.
*/
protected String buildJson() {
ObjectNode root = OBJECT_MAPPER.createObjectNode();
root.put("version", SCHEMA_VERSION);
populateConfig(root, context.getConfig().getDefaultProfile());
try {
return OBJECT_MAPPER.writeValueAsString(root);
} catch (JsonProcessingException e) {
// An in-memory node tree should never fail to serialize; never let it break connection setup.
LOG.warn("Failed to serialize driver configuration report; skipping DRIVER_CONFIG", e);
return null;
}
}

/**
* Populates the configuration groups onto the report root. Placeholder in Stage 1; Stage 2 fills
* in {@code connection}, {@code socket}, the policy groups, {@code query_defaults}, {@code tls},
* etc.
*/
protected void populateConfig(ObjectNode root, DriverExecutionProfile config) {
// Stage 2: populate configuration groups from `config` and the context's policies.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ public class DefaultDriverContext implements InternalDriverContext {
new LazyReference<>("requestThrottler", this::buildRequestThrottler, cycleDetector);
private final LazyReference<Map<String, String>> startupOptionsRef =
new LazyReference<>("startupOptions", this::buildStartupOptions, cycleDetector);
private final LazyReference<DriverConfigReporter> driverConfigReporterRef =
new LazyReference<>("driverConfigReporter", this::buildDriverConfigReporter, cycleDetector);
private final LazyReference<NodeStateListener> nodeStateListenerRef;
private final LazyReference<SchemaChangeListener> schemaChangeListenerRef;
private final LazyReference<RequestTracker> requestTrackerRef;
Expand Down Expand Up @@ -369,6 +371,10 @@ protected Map<String, String> buildStartupOptions() {
.build();
}

protected DriverConfigReporter buildDriverConfigReporter() {
return new DefaultDriverConfigReporter(this);
}

protected Map<String, LoadBalancingPolicy> buildLoadBalancingPolicies() {
return Reflection.buildFromConfigProfiles(
this,
Expand Down Expand Up @@ -1226,6 +1232,12 @@ public Map<String, String> getStartupOptions() {
return startupOptionsRef.get();
}

@NonNull
@Override
public DriverConfigReporter getDriverConfigReporter() {
return driverConfigReporterRef.get();
}

protected RequestLogFormatter buildRequestLogFormatter() {
return new RequestLogFormatter(this);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.datastax.oss.driver.internal.core.context;

import java.util.Map;

/**
* Adds the client-configuration-reporting entries to a connection's CQL {@code STARTUP} options, so
* ScyllaDB can store them in {@code system.clients.client_options} and operators can inspect a
* client's effective driver settings while investigating incidents.
*
* <p>Two entries are produced, both governed by {@code advanced.driver-config-reporting.enabled}:
*
* <ul>
* <li>{@code SESSION_ID} — a unique-per-session identifier, added on <em>every</em> connection so
* the server can group all of a session's connections;
* <li>{@code DRIVER_CONFIG} — the full configuration JSON blob, added only on the control
* connection (pooled connections are correlated back to it via {@code SESSION_ID}).
* </ul>
*/
public interface DriverConfigReporter {

/**
* Adds the reporting entries to the given startup options: {@code SESSION_ID} on every
* connection, plus {@code DRIVER_CONFIG} when {@code reportDriverConfig} is true (the control
* connection). Does nothing when configuration reporting is disabled.
*
* <p>Called from the protocol-initialization handler for every connection.
*
* <p><b>Implementations must not throw:</b> this runs on the connection initialization path, so a
* failure to build the report must be swallowed (and logged) rather than propagated, otherwise it
* would prevent the session from establishing or reconnecting.
*
* @param reportDriverConfig whether this connection should also carry the full {@code
* DRIVER_CONFIG} blob; true only for the control connection.
*/
void populateStartupOptions(Map<String, String> startupOptions, boolean reportDriverConfig);
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@ public interface InternalDriverContext extends DriverContext {
@NonNull
Map<String, String> getStartupOptions();

/**
* The component that builds the driver-configuration report ({@code DRIVER_CONFIG}) sent on the
* control connection's Startup message.
*/
@NonNull
DriverConfigReporter getDriverConfigReporter();

/**
* A list of additional components to notify of session lifecycle events.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ private void init(
DriverChannelOptions.builder()
.withEvents(eventTypes, ControlConnection.this)
.withOwnerLogPrefix(logPrefix + "|control")
.reportConfig(true)
.build();

Queue<Node> nodes =
Expand Down
20 changes: 20 additions & 0 deletions core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -1204,6 +1204,26 @@ datastax-java-driver {

}

advanced.driver-config-reporting {

# Whether the driver reports its effective configuration to ScyllaDB at connection time.
#
# When true, the driver adds two entries to the CQL STARTUP options, which ScyllaDB stores in
# system.clients.client_options so operators can inspect driver settings while investigating
# incidents: a SESSION_ID on every connection (so the server can group a session's connections)
# and a compact JSON payload under the DRIVER_CONFIG key on the control connection only. At this
# stage the DRIVER_CONFIG payload carries only schema-version metadata ({"version":1}); reporting
# of the effective configuration fields is planned for a later stage. When false, neither entry
# is sent and there is no change on the wire.
#
# Required: no
# Modifiable at runtime: yes, the new value will be used for connections initialized after the change.
# Overridable in a profile: no
# Default: false
enabled = false

}

# Whether to resolve the addresses passed to `basic.contact-points`.
#
# If this is true, addresses are created with `InetSocketAddress(String, int)`: the host name will
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ public void setup() throws InterruptedException {
when(context.getEventBus()).thenReturn(eventBus);
when(context.getWriteCoalescer()).thenReturn(new PassThroughWriteCoalescer(null));
when(context.getCompressor()).thenReturn(compressor);
// The init handler consults the config reporter for every connection; default to a no-op.
when(context.getDriverConfigReporter()).thenReturn((startupOptions, reportDriverConfig) -> {});

// Start local server
ServerBootstrap serverBootstrap =
Expand Down
Loading
Loading