Skip to content

Commit ada84cb

Browse files
committed
JAVA-2266: Add support for server handshake to send client metadata, including driver, os, platform, and application name
1 parent 604596d commit ada84cb

File tree

38 files changed

+785
-93
lines changed

38 files changed

+785
-93
lines changed

build.gradle

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,31 @@ configure(subprojects.findAll { it.name != 'util' }) {
111111
}
112112
}
113113

114+
def getGitVersion() {
115+
def outputAsString
116+
new ByteArrayOutputStream().withStream { os ->
117+
def result = exec {
118+
executable 'git'
119+
args 'describe', '--tags'
120+
standardOutput = os
121+
}
122+
outputAsString = os.toString().substring(1)
123+
}
124+
return outputAsString
125+
}
126+
127+
configure(subprojects.findAll { it.name != 'util' }) {
128+
129+
project.ext.generatedResources = "$buildDir/generated-resources/main"
130+
131+
task generateVersionPropertiesFile << {
132+
def directory = new File(project.ext.generatedResources)
133+
directory.mkdirs()
134+
def propertiesFile = new File(directory, "version.properties")
135+
propertiesFile.text = "version=${getGitVersion()}"
136+
}
137+
}
138+
114139
configure(subprojects.findAll { it.name != 'util' && it.name != 'mongo-java-driver' }) {
115140
apply plugin: 'checkstyle'
116141
apply plugin: 'findbugs'
@@ -131,6 +156,7 @@ configure(subprojects.findAll { it.name != 'util' && it.name != 'mongo-java-driv
131156
sourceSets {
132157
main {
133158
java.srcDirs = ['src/main']
159+
output.dir(project.ext.generatedResources, builtBy: 'generateVersionPropertiesFile')
134160
}
135161
test {
136162
groovy.srcDirs = ['src/test/functional', 'src/test/unit']

driver-async/src/main/com/mongodb/async/client/MongoClientSettings.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@
3333
import com.mongodb.event.CommandListener;
3434
import org.bson.codecs.configuration.CodecRegistry;
3535

36+
import java.nio.charset.Charset;
3637
import java.util.ArrayList;
3738
import java.util.Collections;
3839
import java.util.List;
3940

41+
import static com.mongodb.assertions.Assertions.isTrueArgument;
4042
import static com.mongodb.assertions.Assertions.notNull;
4143

4244

@@ -62,6 +64,7 @@ public final class MongoClientSettings {
6264
private final ConnectionPoolSettings connectionPoolSettings;
6365
private final ServerSettings serverSettings;
6466
private final SslSettings sslSettings;
67+
private final String applicationName;
6568

6669
/**
6770
* Convenience method to create a Builder.
@@ -105,6 +108,7 @@ public static final class Builder {
105108
private ServerSettings serverSettings = ServerSettings.builder().build();
106109
private SslSettings sslSettings = SslSettings.builder().build();
107110
private List<MongoCredential> credentialList = Collections.emptyList();
111+
private String applicationName;
108112

109113
private Builder() {
110114
}
@@ -129,6 +133,7 @@ private Builder(final MongoClientSettings settings) {
129133
heartbeatSocketSettings = settings.getHeartbeatSocketSettings();
130134
connectionPoolSettings = settings.getConnectionPoolSettings();
131135
sslSettings = settings.getSslSettings();
136+
applicationName = settings.getApplicationName();
132137
}
133138

134139
/**
@@ -292,6 +297,27 @@ public Builder addCommandListener(final CommandListener commandListener) {
292297
return this;
293298
}
294299

300+
301+
/**
302+
* Sets the logical name of the application using this MongoClient. The application name may be used by the client to identify
303+
* the application to the server, for use in server logs, slow query logs, and profile collection.
304+
*
305+
* @param applicationName the logical name of the application using this MongoClient. It may be null.
306+
* The UTF-8 encoding may not exceed 128 bytes.
307+
* @return {@code this}
308+
* @see #getApplicationName()
309+
* @since 3.4
310+
* @mongodb.server.release 3.4
311+
*/
312+
public Builder applicationName(final String applicationName) {
313+
if (applicationName != null) {
314+
isTrueArgument("applicationName UTF-8 encoding length <= 128",
315+
applicationName.getBytes(Charset.forName("UTF-8")).length <= 128);
316+
}
317+
this.applicationName = applicationName;
318+
return this;
319+
}
320+
295321
/**
296322
* Build an instance of {@code MongoClientSettings}.
297323
*
@@ -391,6 +417,21 @@ public List<CommandListener> getCommandListeners() {
391417
return Collections.unmodifiableList(commandListeners);
392418
}
393419

420+
/**
421+
* Gets the logical name of the application using this MongoClient. The application name may be used by the client to identify
422+
* the application to the server, for use in server logs, slow query logs, and profile collection.
423+
*
424+
* <p>Default is null.</p>
425+
*
426+
* @return the application name, which may be null
427+
* @since 3.4
428+
* @mongodb.server.release 3.4
429+
*/
430+
public String getApplicationName() {
431+
return applicationName;
432+
}
433+
434+
394435
/**
395436
* Gets the cluster settings.
396437
*
@@ -463,6 +504,7 @@ private MongoClientSettings(final Builder builder) {
463504
streamFactoryFactory = builder.streamFactoryFactory;
464505
codecRegistry = builder.codecRegistry;
465506
commandListeners = builder.commandListeners;
507+
applicationName = builder.applicationName;
466508
clusterSettings = builder.clusterSettings;
467509
serverSettings = builder.serverSettings;
468510
socketSettings = builder.socketSettings;

driver-async/src/main/com/mongodb/async/client/MongoClients.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ public static MongoClient create(final ConnectionString connectionString) {
138138
if (connectionString.getWriteConcern() != null) {
139139
builder.writeConcern(connectionString.getWriteConcern());
140140
}
141+
if (connectionString.getApplicationName() != null) {
142+
builder.applicationName(connectionString.getApplicationName());
143+
}
141144
return create(builder.build());
142145
}
143146

@@ -173,7 +176,8 @@ private static Cluster createCluster(final MongoClientSettings settings, final S
173176
settings.getConnectionPoolSettings(), streamFactory,
174177
heartbeatStreamFactory,
175178
settings.getCredentialList(), null, new JMXConnectionPoolListener(), null,
176-
createCommandListener(settings.getCommandListeners()));
179+
createCommandListener(settings.getCommandListeners()),
180+
settings.getApplicationName());
177181
}
178182

179183
private static StreamFactory getHeartbeatStreamFactory(final MongoClientSettings settings) {

driver-async/src/test/functional/com/mongodb/async/client/MongoClientsSpecification.groovy

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,17 @@ import com.mongodb.ServerAddress
2323
import com.mongodb.WriteConcern
2424
import com.mongodb.connection.AsynchronousSocketChannelStreamFactoryFactory
2525
import com.mongodb.connection.netty.NettyStreamFactoryFactory
26+
import org.bson.Document
2627
import spock.lang.IgnoreIf
2728
import spock.lang.Unroll
2829

2930
import static com.mongodb.ClusterFixture.getSslSettings
31+
import static com.mongodb.ClusterFixture.isStandalone
32+
import static com.mongodb.ClusterFixture.serverVersionAtLeast
3033
import static com.mongodb.ReadPreference.primary
3134
import static com.mongodb.ReadPreference.secondaryPreferred
35+
import static com.mongodb.async.client.Fixture.getMongoClientBuilderFromConnectionString
36+
import static com.mongodb.async.client.TestHelper.run
3237
import static java.util.concurrent.TimeUnit.MILLISECONDS
3338

3439
class MongoClientsSpecification extends FunctionalSpecification {
@@ -174,4 +179,44 @@ class MongoClientsSpecification extends FunctionalSpecification {
174179
'mongodb://localhost' | WriteConcern.ACKNOWLEDGED
175180
'mongodb://localhost/?w=majority' | WriteConcern.MAJORITY
176181
}
182+
183+
@Unroll
184+
def 'should apply application name from connection string to settings'() {
185+
when:
186+
def client = MongoClients.create(uri)
187+
188+
then:
189+
client.settings.getApplicationName() == applicationName
190+
191+
cleanup:
192+
client?.close()
193+
194+
where:
195+
uri | applicationName
196+
'mongodb://localhost' | null
197+
'mongodb://localhost/?appname=app1' | 'app1'
198+
}
199+
200+
@IgnoreIf({ !serverVersionAtLeast([3, 3, 9]) || !isStandalone() })
201+
def 'application name should appear in the system.profile collection'() {
202+
given:
203+
def appName = 'appName1'
204+
def client = MongoClients.create(getMongoClientBuilderFromConnectionString().applicationName(appName).build())
205+
def database = client.getDatabase(getDatabaseName())
206+
def collection = database.getCollection(getCollectionName())
207+
run(database.&runCommand, new Document('profile', 2))
208+
209+
when:
210+
run(collection.&count)
211+
212+
then:
213+
Document profileDocument = run(database.getCollection('system.profile').find().&first)
214+
profileDocument.get('appName') == appName
215+
216+
cleanup:
217+
if (database != null) {
218+
run(database.&runCommand, new Document('profile', 0))
219+
}
220+
client?.close()
221+
}
177222
}

driver-async/src/test/unit/com/mongodb/async/client/MongoClientSettingsSpecification.groovy

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class MongoClientSettingsSpecification extends Specification {
4646
options.getReadConcern() == ReadConcern.DEFAULT
4747
options.getReadPreference() == ReadPreference.primary()
4848
options.getCommandListeners().isEmpty()
49+
options.getApplicationName() == null
4950
options.connectionPoolSettings == ConnectionPoolSettings.builder().build()
5051
options.socketSettings == SocketSettings.builder().build()
5152
options.heartbeatSocketSettings == SocketSettings.builder().build()
@@ -139,6 +140,7 @@ class MongoClientSettingsSpecification extends Specification {
139140
.readPreference(ReadPreference.secondary())
140141
.writeConcern(WriteConcern.JOURNALED)
141142
.readConcern(ReadConcern.LOCAL)
143+
.applicationName('app1')
142144
.addCommandListener(commandListener)
143145
.sslSettings(sslSettings)
144146
.socketSettings(socketSettings)
@@ -155,6 +157,7 @@ class MongoClientSettingsSpecification extends Specification {
155157
options.getReadPreference() == ReadPreference.secondary()
156158
options.getWriteConcern() == WriteConcern.JOURNALED
157159
options.getReadConcern() == ReadConcern.LOCAL
160+
options.getApplicationName() == 'app1'
158161
options.commandListeners.get(0) == commandListener
159162
options.connectionPoolSettings == connectionPoolSettings
160163
options.socketSettings == socketSettings
@@ -183,6 +186,7 @@ class MongoClientSettingsSpecification extends Specification {
183186
.readPreference(ReadPreference.secondary())
184187
.writeConcern(WriteConcern.JOURNALED)
185188
.readConcern(ReadConcern.LOCAL)
189+
.applicationName('app1')
186190
.addCommandListener(commandListener)
187191
.sslSettings(sslSettings)
188192
.socketSettings(socketSettings)
@@ -198,6 +202,29 @@ class MongoClientSettingsSpecification extends Specification {
198202
expect options, isTheSameAs(MongoClientSettings.builder(options).build())
199203
}
200204

205+
def 'applicationName can be 128 bytes when encoded as UTF-8'() {
206+
given:
207+
def applicationName = 'a' * 126 + '\u00A0'
208+
209+
when:
210+
def options = MongoClientSettings.builder().applicationName(applicationName).build()
211+
212+
then:
213+
options.applicationName == applicationName
214+
}
215+
216+
def 'should throw IllegalArgumentException if applicationName exceeds 128 bytes when encoded as UTF-8'() {
217+
given:
218+
def applicationName = 'a' * 127 + '\u00A0'
219+
220+
when:
221+
MongoClientSettings.builder().applicationName(applicationName)
222+
223+
then:
224+
thrown(IllegalArgumentException)
225+
}
226+
227+
201228
def 'should add command listeners'() {
202229
given:
203230
CommandListener commandListenerOne = Mock(CommandListener)
@@ -249,9 +276,9 @@ class MongoClientSettingsSpecification extends Specification {
249276
when:
250277
// A regression test so that if anymore methods are added then the builder(final MongoClientSettings settings) should be updated
251278
def actual = MongoClientSettings.Builder.declaredFields.grep { !it.synthetic } *.name.sort()
252-
def expected = ['clusterSettings', 'codecRegistry', 'commandListeners', 'connectionPoolSettings', 'credentialList',
253-
'heartbeatSocketSettings', 'readConcern', 'readPreference', 'serverSettings', 'socketSettings', 'sslSettings',
254-
'streamFactoryFactory', 'writeConcern']
279+
def expected = ['applicationName', 'clusterSettings', 'codecRegistry', 'commandListeners', 'connectionPoolSettings',
280+
'credentialList', 'heartbeatSocketSettings', 'readConcern', 'readPreference', 'serverSettings', 'socketSettings',
281+
'sslSettings', 'streamFactoryFactory', 'writeConcern']
255282

256283
then:
257284
actual == expected

driver-core/src/main/com/mongodb/ConnectionString.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,11 @@
178178
* Deprecated, please use {@code authMechanismProperties=SERVICE_NAME:string} instead.
179179
* </li>
180180
* </ul>
181+
* <p>Server Handshake configuration:</p>
182+
* <ul>
183+
* <li>{@code appName=string}: Sets the logical name of the application. The application name may be used by the client to identify
184+
* the application to the server, for use in server logs, slow query logs, and profile collection.</li>
185+
* </ul>
181186
*
182187
* @mongodb.driver.manual reference/connection-string Connection String Format
183188
* @since 3.0.0
@@ -214,6 +219,7 @@ public class ConnectionString {
214219
private Integer serverSelectionTimeout;
215220
private Integer localThreshold;
216221
private Integer heartbeatFrequency;
222+
private String applicationName;
217223

218224
/**
219225
* Creates a ConnectionString from the given string.
@@ -329,6 +335,8 @@ public ConnectionString(final String connectionString) {
329335
GENERAL_OPTIONS_KEYS.add("localthresholdms");
330336
GENERAL_OPTIONS_KEYS.add("heartbeatfrequencyms");
331337

338+
GENERAL_OPTIONS_KEYS.add("appname");
339+
332340
READ_PREFERENCE_KEYS.add("readpreference");
333341
READ_PREFERENCE_KEYS.add("readpreferencetags");
334342
READ_PREFERENCE_KEYS.add("maxstalenessms");
@@ -399,6 +407,8 @@ private void translateOptions(final Map<String, List<String>> optionsMap) {
399407
localThreshold = parseInteger(value, "localthresholdms");
400408
} else if (key.equals("heartbeatfrequencyms")) {
401409
heartbeatFrequency = parseInteger(value, "heartbeatfrequencyms");
410+
} else if (key.equals("appname")) {
411+
applicationName = value;
402412
}
403413
}
404414

@@ -975,6 +985,19 @@ public Integer getHeartbeatFrequency() {
975985
return heartbeatFrequency;
976986
}
977987

988+
/**
989+
* Gets the logical name of the application. The application name may be used by the client to identify the application to the server,
990+
* for use in server logs, slow query logs, and profile collection.
991+
*
992+
* <p>Default is null.</p>
993+
*
994+
* @return the application name, which may be null
995+
* @since 3.4
996+
* @mongodb.server.release 3.4
997+
*/
998+
public String getApplicationName() {
999+
return applicationName;
1000+
}
9781001

9791002
@Override
9801003
public String toString() {
@@ -1047,6 +1070,9 @@ public boolean equals(final Object o) {
10471070
if (writeConcern != null ? !writeConcern.equals(that.writeConcern) : that.writeConcern != null) {
10481071
return false;
10491072
}
1073+
if (applicationName != null ? !applicationName.equals(that.applicationName) : that.applicationName != null) {
1074+
return false;
1075+
}
10501076

10511077
return true;
10521078
}
@@ -1071,6 +1097,7 @@ public int hashCode() {
10711097
result = 31 * result + (socketTimeout != null ? socketTimeout.hashCode() : 0);
10721098
result = 31 * result + (sslEnabled != null ? sslEnabled.hashCode() : 0);
10731099
result = 31 * result + (requiredReplicaSetName != null ? requiredReplicaSetName.hashCode() : 0);
1100+
result = 31 * result + (applicationName != null ? applicationName.hashCode() : 0);
10741101
return result;
10751102
}
10761103
}

0 commit comments

Comments
 (0)