Skip to content

Commit f81d290

Browse files
committed
Auto-configure the Postgres application_name when using Docker Compose
1 parent 68022ef commit f81d290

File tree

9 files changed

+384
-23
lines changed

9 files changed

+384
-23
lines changed

spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java

+24-3
Original file line numberDiff line numberDiff line change
@@ -57,22 +57,43 @@ void runWithBitnamiImageCreatesConnectionDetails(JdbcConnectionDetails connectio
5757
assertConnectionDetails(connectionDetails);
5858
}
5959

60+
@DockerComposeTest(composeFile = "postgres-application-name-compose.yaml", image = TestImage.POSTGRESQL)
61+
void runCreatesConnectionDetailsApplicationName(JdbcConnectionDetails connectionDetails)
62+
throws ClassNotFoundException {
63+
assertThat(connectionDetails.getUsername()).isEqualTo("myuser");
64+
assertThat(connectionDetails.getPassword()).isEqualTo("secret");
65+
assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:postgresql://")
66+
.endsWith("?ApplicationName=spring+boot");
67+
checkApplicationName(connectionDetails, "spring boot");
68+
}
69+
6070
private void assertConnectionDetails(JdbcConnectionDetails connectionDetails) {
6171
assertThat(connectionDetails.getUsername()).isEqualTo("myuser");
6272
assertThat(connectionDetails.getPassword()).isEqualTo("secret");
6373
assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:postgresql://").endsWith("/mydatabase");
6474
}
6575

66-
@SuppressWarnings("unchecked")
6776
private void checkDatabaseAccess(JdbcConnectionDetails connectionDetails) throws ClassNotFoundException {
77+
assertThat(queryForObject(connectionDetails, DatabaseDriver.POSTGRESQL.getValidationQuery(), Integer.class))
78+
.isEqualTo(1);
79+
}
80+
81+
private void checkApplicationName(JdbcConnectionDetails connectionDetails, String applicationName)
82+
throws ClassNotFoundException {
83+
assertThat(queryForObject(connectionDetails, "select current_setting('application_name')", String.class))
84+
.isEqualTo(applicationName);
85+
}
86+
87+
@SuppressWarnings("unchecked")
88+
private <T> T queryForObject(JdbcConnectionDetails connectionDetails, String sql, Class<T> result)
89+
throws ClassNotFoundException {
6890
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
6991
dataSource.setUrl(connectionDetails.getJdbcUrl());
7092
dataSource.setUsername(connectionDetails.getUsername());
7193
dataSource.setPassword(connectionDetails.getPassword());
7294
dataSource.setDriverClass((Class<? extends Driver>) ClassUtils.forName(connectionDetails.getDriverClassName(),
7395
getClass().getClassLoader()));
74-
JdbcTemplate template = new JdbcTemplate(dataSource);
75-
assertThat(template.queryForObject(DatabaseDriver.POSTGRESQL.getValidationQuery(), Integer.class)).isEqualTo(1);
96+
return new JdbcTemplate(dataSource).queryForObject(sql, result);
7697
}
7798

7899
}

spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java

+30-8
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import io.r2dbc.spi.ConnectionFactories;
2222
import io.r2dbc.spi.ConnectionFactoryOptions;
23+
import io.r2dbc.spi.Option;
2324

2425
import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails;
2526
import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest;
@@ -60,21 +61,42 @@ void runWithBitnamiImageCreatesConnectionDetails(R2dbcConnectionDetails connecti
6061
assertConnectionDetails(connectionDetails);
6162
}
6263

64+
@DockerComposeTest(composeFile = "postgres-application-name-compose.yaml", image = TestImage.POSTGRESQL)
65+
void runCreatesConnectionDetailsApplicationName(R2dbcConnectionDetails connectionDetails) {
66+
assertConnectionDetails(connectionDetails);
67+
ConnectionFactoryOptions options = connectionDetails.getConnectionFactoryOptions();
68+
assertThat(options.getValue(Option.valueOf("applicationName"))).isEqualTo("spring boot");
69+
checkApplicationName(connectionDetails, "spring boot");
70+
}
71+
6372
private void assertConnectionDetails(R2dbcConnectionDetails connectionDetails) {
64-
ConnectionFactoryOptions connectionFactoryOptions = connectionDetails.getConnectionFactoryOptions();
65-
assertThat(connectionFactoryOptions.toString()).contains("database=mydatabase", "driver=postgresql",
66-
"password=REDACTED", "user=myuser");
67-
assertThat(connectionFactoryOptions.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("secret");
73+
ConnectionFactoryOptions options = connectionDetails.getConnectionFactoryOptions();
74+
assertThat(options.getRequiredValue(ConnectionFactoryOptions.HOST)).isNotNull();
75+
assertThat(options.getRequiredValue(ConnectionFactoryOptions.PORT)).isNotNull();
76+
assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("mydatabase");
77+
assertThat(options.getRequiredValue(ConnectionFactoryOptions.USER)).isEqualTo("myuser");
78+
assertThat(options.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("secret");
79+
assertThat(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)).isEqualTo("postgresql");
6880
}
6981

7082
private void checkDatabaseAccess(R2dbcConnectionDetails connectionDetails) {
83+
Integer result = queryForObject(connectionDetails, DatabaseDriver.POSTGRESQL.getValidationQuery(),
84+
Integer.class);
85+
assertThat(result).isEqualTo(1);
86+
}
87+
88+
private void checkApplicationName(R2dbcConnectionDetails connectionDetails, String applicationName) {
89+
assertThat(queryForObject(connectionDetails, "select current_setting('application_name')", String.class))
90+
.isEqualTo(applicationName);
91+
}
92+
93+
private <T> T queryForObject(R2dbcConnectionDetails connectionDetails, String sql, Class<T> result) {
7194
ConnectionFactoryOptions connectionFactoryOptions = connectionDetails.getConnectionFactoryOptions();
72-
Object result = DatabaseClient.create(ConnectionFactories.get(connectionFactoryOptions))
73-
.sql(DatabaseDriver.POSTGRESQL.getValidationQuery())
74-
.map((row, metadata) -> row.get(0))
95+
return DatabaseClient.create(ConnectionFactories.get(connectionFactoryOptions))
96+
.sql(sql)
97+
.mapValue(result)
7598
.first()
7699
.block(Duration.ofSeconds(30));
77-
assertThat(result).isEqualTo(1);
78100
}
79101

80102
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
services:
2+
database:
3+
image: '{imageName}'
4+
ports:
5+
- '5432'
6+
environment:
7+
- 'POSTGRES_USER=myuser'
8+
- 'POSTGRES_DB=mydatabase'
9+
- 'POSTGRES_PASSWORD=secret'
10+
labels:
11+
org.springframework.boot.jdbc.parameters: 'ApplicationName=spring+boot'
12+
org.springframework.boot.r2dbc.parameters: 'applicationName=spring boot'

spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionSource.java

+15-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.boot.docker.compose.service.connection;
1818

1919
import org.springframework.boot.docker.compose.core.RunningService;
20+
import org.springframework.core.env.Environment;
2021

2122
/**
2223
* Passed to {@link DockerComposeConnectionDetailsFactory} to provide details of the
@@ -32,12 +33,16 @@ public final class DockerComposeConnectionSource {
3233

3334
private final RunningService runningService;
3435

36+
private final Environment environment;
37+
3538
/**
3639
* Create a new {@link DockerComposeConnectionSource} instance.
3740
* @param runningService the running Docker Compose service
41+
* @param environment environment in which the current application is running
3842
*/
39-
DockerComposeConnectionSource(RunningService runningService) {
43+
DockerComposeConnectionSource(RunningService runningService, Environment environment) {
4044
this.runningService = runningService;
45+
this.environment = environment;
4146
}
4247

4348
/**
@@ -48,4 +53,13 @@ public RunningService getRunningService() {
4853
return this.runningService;
4954
}
5055

56+
/**
57+
* Environment in which the current application is running.
58+
* @return the environment
59+
* @since 3.5.0
60+
*/
61+
public Environment getEnvironment() {
62+
return this.environment;
63+
}
64+
5165
}

spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeServiceConnectionsApplicationListener.java

+6-3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.boot.docker.compose.lifecycle.DockerComposeServicesReadyEvent;
3232
import org.springframework.context.ApplicationContext;
3333
import org.springframework.context.ApplicationListener;
34+
import org.springframework.core.env.Environment;
3435
import org.springframework.util.ClassUtils;
3536
import org.springframework.util.StringUtils;
3637

@@ -59,13 +60,15 @@ class DockerComposeServiceConnectionsApplicationListener
5960
public void onApplicationEvent(DockerComposeServicesReadyEvent event) {
6061
ApplicationContext applicationContext = event.getSource();
6162
if (applicationContext instanceof BeanDefinitionRegistry registry) {
62-
registerConnectionDetails(registry, event.getRunningServices());
63+
Environment environment = applicationContext.getEnvironment();
64+
registerConnectionDetails(registry, environment, event.getRunningServices());
6365
}
6466
}
6567

66-
private void registerConnectionDetails(BeanDefinitionRegistry registry, List<RunningService> runningServices) {
68+
private void registerConnectionDetails(BeanDefinitionRegistry registry, Environment environment,
69+
List<RunningService> runningServices) {
6770
for (RunningService runningService : runningServices) {
68-
DockerComposeConnectionSource source = new DockerComposeConnectionSource(runningService);
71+
DockerComposeConnectionSource source = new DockerComposeConnectionSource(runningService, environment);
6972
this.factories.getConnectionDetails(source, false).forEach((connectionDetailsType, connectionDetails) -> {
7073
register(registry, runningService, connectionDetailsType, connectionDetails);
7174
this.factories.getConnectionDetails(connectionDetails, false)

spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactory.java

+30-3
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,16 @@
1616

1717
package org.springframework.boot.docker.compose.service.connection.postgres;
1818

19+
import java.net.URLEncoder;
20+
import java.nio.charset.StandardCharsets;
21+
1922
import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails;
2023
import org.springframework.boot.docker.compose.core.RunningService;
2124
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
2225
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
2326
import org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder;
27+
import org.springframework.core.env.Environment;
28+
import org.springframework.util.StringUtils;
2429

2530
/**
2631
* {@link DockerComposeConnectionDetailsFactory} to create {@link JdbcConnectionDetails}
@@ -42,7 +47,7 @@ protected PostgresJdbcDockerComposeConnectionDetailsFactory() {
4247

4348
@Override
4449
protected JdbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
45-
return new PostgresJdbcDockerComposeConnectionDetails(source.getRunningService());
50+
return new PostgresJdbcDockerComposeConnectionDetails(source.getRunningService(), source.getEnvironment());
4651
}
4752

4853
/**
@@ -57,10 +62,11 @@ static class PostgresJdbcDockerComposeConnectionDetails extends DockerComposeCon
5762

5863
private final String jdbcUrl;
5964

60-
PostgresJdbcDockerComposeConnectionDetails(RunningService service) {
65+
PostgresJdbcDockerComposeConnectionDetails(RunningService service, Environment environment) {
6166
super(service);
6267
this.environment = new PostgresEnvironment(service.env());
63-
this.jdbcUrl = jdbcUrlBuilder.build(service, this.environment.getDatabase());
68+
this.jdbcUrl = addApplicationNameIfNecessary(jdbcUrlBuilder.build(service, this.environment.getDatabase()),
69+
environment);
6470
}
6571

6672
@Override
@@ -78,6 +84,27 @@ public String getJdbcUrl() {
7884
return this.jdbcUrl;
7985
}
8086

87+
private static String addApplicationNameIfNecessary(String jdbcUrl, Environment environment) {
88+
if (jdbcUrl.contains("&ApplicationName=") || jdbcUrl.contains("?ApplicationName=")) {
89+
return jdbcUrl;
90+
}
91+
String applicationName = environment.getProperty("spring.application.name");
92+
if (!StringUtils.hasText(applicationName)) {
93+
return jdbcUrl;
94+
}
95+
StringBuilder jdbcUrlBuilder = new StringBuilder(jdbcUrl);
96+
if (!jdbcUrl.contains("?")) {
97+
jdbcUrlBuilder.append("?");
98+
}
99+
else if (!jdbcUrl.endsWith("&")) {
100+
jdbcUrlBuilder.append("&");
101+
}
102+
return jdbcUrlBuilder.append("ApplicationName")
103+
.append('=')
104+
.append(URLEncoder.encode(applicationName, StandardCharsets.UTF_8))
105+
.toString();
106+
}
107+
81108
}
82109

83110
}

spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactory.java

+28-5
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@
1717
package org.springframework.boot.docker.compose.service.connection.postgres;
1818

1919
import io.r2dbc.spi.ConnectionFactoryOptions;
20+
import io.r2dbc.spi.Option;
2021

2122
import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails;
2223
import org.springframework.boot.docker.compose.core.RunningService;
2324
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
2425
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
2526
import org.springframework.boot.docker.compose.service.connection.r2dbc.ConnectionFactoryOptionsBuilder;
27+
import org.springframework.core.env.Environment;
28+
import org.springframework.util.StringUtils;
2629

2730
/**
2831
* {@link DockerComposeConnectionDetailsFactory} to create {@link R2dbcConnectionDetails}
@@ -44,7 +47,7 @@ class PostgresR2dbcDockerComposeConnectionDetailsFactory
4447

4548
@Override
4649
protected R2dbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
47-
return new PostgresDbR2dbcDockerComposeConnectionDetails(source.getRunningService());
50+
return new PostgresDbR2dbcDockerComposeConnectionDetails(source.getRunningService(), source.getEnvironment());
4851
}
4952

5053
/**
@@ -53,23 +56,43 @@ protected R2dbcConnectionDetails getDockerComposeConnectionDetails(DockerCompose
5356
static class PostgresDbR2dbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails
5457
implements R2dbcConnectionDetails {
5558

59+
private static final Option<String> APPLICATION_NAME = Option.valueOf("applicationName");
60+
5661
private static final ConnectionFactoryOptionsBuilder connectionFactoryOptionsBuilder = new ConnectionFactoryOptionsBuilder(
5762
"postgresql", 5432);
5863

5964
private final ConnectionFactoryOptions connectionFactoryOptions;
6065

61-
PostgresDbR2dbcDockerComposeConnectionDetails(RunningService service) {
66+
PostgresDbR2dbcDockerComposeConnectionDetails(RunningService service, Environment environment) {
6267
super(service);
63-
PostgresEnvironment environment = new PostgresEnvironment(service.env());
64-
this.connectionFactoryOptions = connectionFactoryOptionsBuilder.build(service, environment.getDatabase(),
65-
environment.getUsername(), environment.getPassword());
68+
this.connectionFactoryOptions = getConnectionFactoryOptions(service, environment);
6669
}
6770

6871
@Override
6972
public ConnectionFactoryOptions getConnectionFactoryOptions() {
7073
return this.connectionFactoryOptions;
7174
}
7275

76+
private static ConnectionFactoryOptions getConnectionFactoryOptions(RunningService service,
77+
Environment environment) {
78+
PostgresEnvironment env = new PostgresEnvironment(service.env());
79+
ConnectionFactoryOptions connectionFactoryOptions = connectionFactoryOptionsBuilder.build(service,
80+
env.getDatabase(), env.getUsername(), env.getPassword());
81+
return addApplicationNameIfNecessary(connectionFactoryOptions, environment);
82+
}
83+
84+
private static ConnectionFactoryOptions addApplicationNameIfNecessary(
85+
ConnectionFactoryOptions connectionFactoryOptions, Environment environment) {
86+
if (connectionFactoryOptions.hasOption(APPLICATION_NAME)) {
87+
return connectionFactoryOptions;
88+
}
89+
String applicationName = environment.getProperty("spring.application.name");
90+
if (!StringUtils.hasText(applicationName)) {
91+
return connectionFactoryOptions;
92+
}
93+
return connectionFactoryOptions.mutate().option(APPLICATION_NAME, applicationName).build();
94+
}
95+
7396
}
7497

7598
}

0 commit comments

Comments
 (0)