diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoKeyName.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoKeyName.java index c416b06cd..9fe29a40b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoKeyName.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoKeyName.java @@ -1,5 +1,5 @@ /* - * Copyright 2025 the original author or authors. + * Copyright 2025-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import org.springframework.util.StringUtils; * {@link KeyValue} and {@link KeyName}. * * @author Mark Paluch + * @since 4.4.9 */ record MongoKeyName(String name, boolean required, Function valueFunction) implements KeyName { @@ -43,7 +44,7 @@ record MongoKeyName(String name, boolean required, Function valueF * @return * @param */ - public static MongoKeyName required(String name, Function valueFunction) { + static MongoKeyName required(String name, Function valueFunction) { return required(name, valueFunction, Objects::nonNull); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservation.java index cf191cf4d..68ceb3cbf 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservation.java @@ -15,11 +15,17 @@ */ package org.springframework.data.mongodb.observability; -import static org.springframework.data.mongodb.observability.MongoKeyName.*; +import static org.springframework.data.mongodb.observability.MongoKeyName.MongoKeyValue; +import static org.springframework.data.mongodb.observability.MongoKeyName.just; import io.micrometer.common.docs.KeyName; import io.micrometer.observation.docs.ObservationDocumentation; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.regex.Pattern; + +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; @@ -86,7 +92,7 @@ enum MongoObservation implements ObservationDocumentation { static MongoKeyName NET_PEER_PORT = MongoKeyName.required("net.peer.port", ServerAddress::getPort); static MongoKeyName DB_CONNECTION_STRING = MongoKeyName.requiredString("db.connection_string", - Object::toString); + MongoObservation::connectionString); static MongoKeyName DB_USER = MongoKeyName.requiredString("db.user", ConnectionString::getUsername); @@ -96,7 +102,7 @@ enum MongoObservation implements ObservationDocumentation { * @param context the context to contribute from, can be {@literal null} if no context is available. * @return the key value contributor providing low cardinality key names. */ - public static Observer observe(@Nullable MongoHandlerContext context) { + static Observer observe(@Nullable MongoHandlerContext context) { return Observer.fromContext(context, it -> { @@ -115,9 +121,51 @@ enum MongoObservation implements ObservationDocumentation { * * @return the key names for low cardinality keys. */ - public static KeyName[] getKeyNames() { + static KeyName[] getKeyNames() { return observe(null).toKeyNames(); } } + @Contract("null -> null") + static @Nullable String connectionString(@Nullable ConnectionString connectionString) { + + if (connectionString == null) { + return null; + } + + if (!StringUtils.hasText(connectionString.getUsername()) && connectionString.getPassword() == null) { + return connectionString.toString(); + } + + String target = renderPart(connectionString.toString(), "//", connectionString.getUsername()); + + if (connectionString.getPassword() != null) { + + String rendered = renderPart(target, ":", new String(connectionString.getPassword())); + if (!rendered.equals(target)) { + target = rendered; + } else { + String protocol = connectionString.isSrvProtocol() ? "mongodb+srv" : "mongodb"; + target = "%s://*****:*****@%s".formatted(protocol, + StringUtils.collectionToCommaDelimitedString(connectionString.getHosts())); + } + } + + return target; + } + + private static String renderPart(String source, String prefix, @Nullable String part) { + + if (!StringUtils.hasText(part)) { + return source; + } + + String intermediate = source.replaceFirst(prefix + Pattern.quote(part), "%s*****".formatted(prefix)); + if (!intermediate.equals(source)) { + return intermediate; + } + + String encoded = URLEncoder.encode(part, StandardCharsets.UTF_8); + return source.replaceFirst(prefix + Pattern.quote(encoded), "%s*****".formatted(prefix)); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/Observer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/Observer.java index b25cacf86..99bc6bd8b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/Observer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/Observer.java @@ -30,6 +30,7 @@ import org.springframework.lang.Nullable; * observability systems. * * @author Mark Paluch + * @since 4.4.9 */ class Observer { @@ -40,7 +41,7 @@ class Observer { * * @return a new {@link Observer}. */ - public static Observer create() { + static Observer create() { return new Observer(); } @@ -53,7 +54,7 @@ class Observer { * @return the stateful {@link Observer}. * @param context type. */ - public static Observer fromContext(@Nullable C context, Consumer> consumer) { + static Observer fromContext(@Nullable C context, Consumer> consumer) { Observer contributor = create(); @@ -68,7 +69,7 @@ class Observer { * @param keyValue * @return */ - public Observer contribute(MongoKeyName.MongoKeyValue keyValue) { + Observer contribute(MongoKeyName.MongoKeyValue keyValue) { keyValues.add(keyValue); @@ -83,7 +84,7 @@ class Observer { * @return the nested contextual {@link ContextualObserver} that can contribute key-value tuples. * @param */ - public ContextualObserver contextual(@Nullable C context) { + ContextualObserver contextual(@Nullable C context) { if (context == null) { return new EmptyContextualObserver<>(keyValues); @@ -92,15 +93,11 @@ class Observer { return new DefaultContextualObserver<>(context, keyValues); } - public ContextualObserver empty(Class targetType) { - return new EmptyContextualObserver<>(this.keyValues); - } - - public KeyValues toKeyValues() { + KeyValues toKeyValues() { return KeyValues.of(keyValues); } - public KeyName[] toKeyNames() { + KeyName[] toKeyNames() { KeyName[] keyNames = new KeyName[keyValues.size()]; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationUnitTests.java new file mode 100644 index 000000000..8202cafff --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationUnitTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed 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 + * + * https://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 org.springframework.data.mongodb.observability; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import com.mongodb.ConnectionString; + +/** + * @author Christoph Strobl + */ +class MongoObservationUnitTests { + + @ParameterizedTest // GH-5020 + @MethodSource("connectionStrings") + void connectionStringRendering(ConnectionString source, String expected) { + assertThat(MongoObservation.connectionString(source)).isEqualTo(expected); + } + + private static Stream connectionStrings() { + + return Stream.of(Arguments.of(new ConnectionString( + "mongodb+srv://m0ngP%40oUser:m0ngP%40o@cluster0.example.mongodb.net/?retryWrites=true&w=majority"), + "mongodb+srv://*****:*****@cluster0.example.mongodb.net/?retryWrites=true&w=majority"), // + Arguments.of(new ConnectionString( + "mongodb://mongodb:m0ngP%40o@cluster0.example.mongodb.net,cluster1.example.com:1234/?retryWrites=true"), + "mongodb://*****:*****@cluster0.example.mongodb.net,cluster1.example.com:1234/?retryWrites=true"), // + Arguments.of( + new ConnectionString("mongodb://myDatabaseUser@cluster0.example.mongodb.net/?authMechanism=MONGODB-X509"), + "mongodb://*****@cluster0.example.mongodb.net/?authMechanism=MONGODB-X509"), // + Arguments.of( + new ConnectionString("mongodb+srv://myDatabaseUser:mongodb@cluster0.example.mongodb.net/?w=acknowledged"), + "mongodb+srv://*****:*****@cluster0.example.mongodb.net/?w=acknowledged"), // + Arguments.of( + new ConnectionString( + new String("mongodb://mongodb:mongodb@localhost:27017".getBytes(), StandardCharsets.US_ASCII)), + "mongodb://*****:*****@localhost:27017"), + Arguments.of(new ConnectionString("mongodb+srv://cluster0.example.mongodb.net/?retryWrites=true&w=majority"), + "mongodb+srv://cluster0.example.mongodb.net/?retryWrites=true&w=majority"), // + Arguments.of( + new ConnectionString( + "mongodb+srv://mongodb:mongodb@cluster0.example.mongodb.net/?retryWrites=true&w=majority"), + "mongodb+srv://*****:*****@cluster0.example.mongodb.net/?retryWrites=true&w=majority")); + } + +}