diff --git a/.gitignore b/.gitignore index b425f09..0573d41 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,5 @@ build/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store +/mise.toml diff --git a/README.md b/README.md index fe331ee..1bde162 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,17 @@ # Spring Boot Properties Logger -_Properties Logger_ is a module for Spring Boot 3+ apps which early logs properties +_Properties Logger_ is a module for Spring Boot 4+ apps which early logs properties detected by Spring Boot and their values resolved by Spring Boot. ## Requirements The _properties Logger_ module is designed to work with : -- Spring Boot 3.3+ (an so java 17+) +- Spring Boot 4.0+ (an so java 17+) - Servlet web application - Reactive web application (not tested) - Batch application application (not tested) - CommandLineRunner application (not tested) -- Spring boot version < 3.3 (not tested) ## Usage @@ -31,29 +30,30 @@ NB : The module module _Properties Logger_ logs its message with properties and their values at the info level : so its **log level must be at least INFO**. DEBUG (or TRACE) give (much) more - informations. + informations : `logging.level.io.github.fbibonne.springaddons.boot = INFO` ## Result You should see this kind of output in your log (in the console by default in a Spring Boot app) : ```text -2024-06-25T12:01:00.858+02:00 INFO 42091 --- [ main] io.github.fbibonne.springaddons.boot.propertieslogger.EnvironmentPreparedEventForPropertiesLogging : +2025-08-25T12:01:00.858+02:00 INFO 42091 --- [ main] io.github.fbibonne.springaddons.boot.propertieslogger.EnvironmentPreparedEventForPropertiesLogging : ================================================================================ Values of properties from sources : - Config resource 'class path resource [application.properties]' via location 'optional:classpath:/' ==== -springdoc.show-actuator = true -springdoc.swagger-ui.path = / -springdoc.pathsToMatch = /** -springdoc.swagger-ui.oauth.clientId = -server.forward-headers-strategy = framework -spring.security.oauth2.resourceserver.jwt.jwk-set-uri = -spring.datasource.username = user -spring.datasource.password = ****** -springdoc.swagger-ui.syntax-highlight.activated = false -management.endpoint.health.show-details = always -logging.level.root = INFO +logging.level.root = INFO ### FROM "logging.level.root" from property source "commandLineArgs" ### +management.endpoint.health.show-details = always ### FROM class path resource [application.properties] - 10:43 ### +server.forward-headers-strategy = framework ### FROM class path resource [application.properties] - 5:35 ### +springdoc.pathsToMatch = /** ### FROM class path resource [application.properties] - 3:26 ### +springdoc.show-actuator = true ### FROM class path resource [application.properties] - 1:27 ### +springdoc.swagger-ui.oauth.clientId = ### FROM class path resource [application.properties] - 4:39 ### +springdoc.swagger-ui.path = / ### FROM class path resource [application.properties] - 2:29 ### +springdoc.swagger-ui.syntax-highlight.activated = false ### FROM class path resource [application.properties] - 9:51 ### +spring.application.pid = 379695 ### FROM "spring.application.pid" from property source "applicationInfo" ### +spring.datasource.password = ****** ### FROM System Environment Property "spring.datasource.password" ### +spring.datasource.username = user ### FROM class path resource [application.properties] - 7:30 ### +spring.security.oauth2.resourceserver.jwt.jwk-set-uri = ### FROM class path resource [application.properties] - 6:57 ### ================================================================================ . ____ _ __ _ _ @@ -63,7 +63,7 @@ logging.level.root = INFO ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ - :: Spring Boot :: (v3.3.1) + :: Spring Boot :: (v4.0.0) ``` The first part of the log message generated by the module (before the separator `====` ) diff --git a/pom.xml b/pom.xml index b29053c..060a0a1 100644 --- a/pom.xml +++ b/pom.xml @@ -7,18 +7,28 @@ org.springframework.boot spring-boot-dependencies - 3.5.5 + 4.0.2 io.github.fbibonne boot-properties-logger-starter - 1.5.0 + 2.0.0 jar + + 1.0.0 17 17 UTF-8 + 3.4.0 + 3.12.0 + 3.2.8 + 0.10.0 Spring Boot Properties Logger @@ -69,6 +79,12 @@ + + org.jspecify + jspecify + ${jspecify.version} + compile + org.springframework.boot spring-boot @@ -107,7 +123,7 @@ org.apache.maven.plugins maven-source-plugin - 3.3.1 + ${maven-source-plugin.version} attach-sources @@ -120,7 +136,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.10.1 + ${maven-javadoc-plugin.version} attach-javadocs @@ -133,7 +149,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.2.8 + ${maven-gpg-plugin.version} bc @@ -150,7 +166,7 @@ org.sonatype.central central-publishing-maven-plugin - 0.8.0 + ${central-publishing-maven-plugin.version} true central diff --git a/src/main/java/io/github/fbibonne/springaddons/boot/propertieslogger/EnvironmentPreparedEventForPropertiesLogging.java b/src/main/java/io/github/fbibonne/springaddons/boot/propertieslogger/EnvironmentPreparedEventForPropertiesLogging.java index e959aa0..416f440 100644 --- a/src/main/java/io/github/fbibonne/springaddons/boot/propertieslogger/EnvironmentPreparedEventForPropertiesLogging.java +++ b/src/main/java/io/github/fbibonne/springaddons/boot/propertieslogger/EnvironmentPreparedEventForPropertiesLogging.java @@ -1,5 +1,6 @@ package io.github.fbibonne.springaddons.boot.propertieslogger; +import org.jspecify.annotations.Nullable; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.context.ApplicationListener; import org.springframework.core.env.*; @@ -24,12 +25,12 @@ public record EnvironmentPreparedEventForPropertiesLogging() implements Applicat private static final Set DEFAULT_PROPS_WITH_HIDDEN_VALUES = Set.of("password", "pwd", "token", "secret", "credential", "pw"); private static final Set DEFAULT_PREFIX_FOR_PROPERTIES = Set.of("debug", "trace", "info", "logging", "spring", "server", "management", "springdoc", "properties"); - private static final Set DEFAULT_SOURCES_IGNORED = Set.of(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME); + public static final Set DEFAULT_SOURCES_IGNORED = Set.of(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME); private static final boolean DEFAULT_PROPERTIES_LOGGER_DISABLED = false; private static final String KEY_FOR_PROPS_WITH_HIDDEN_VALUES = "properties.logger.with-hidden-values"; private static final String KEY_FOR_PREFIX_FOR_PROPERTIES = "properties.logger.prefix-for-properties"; - private static final String KEY_FOR_SOURCES_IGNORED = "properties.logger.sources-ignored"; + public static final String KEY_FOR_SOURCES_IGNORED = "properties.logger.sources-ignored"; public static final String KEY_FOR_DISABLED = "properties.logger.disabled"; @Override @@ -61,19 +62,29 @@ private void doLogProperties(CustomAbstractEnvironment abstractEnvironment) { final AllowedPrefixForProperties allowedPrefixForProperties = new AllowedPrefixForProperties(getPropertyOrDefaultAndTrace(abstractEnvironment, KEY_FOR_PREFIX_FOR_PROPERTIES, Set.class, DEFAULT_PREFIX_FOR_PROPERTIES)); final IgnoredPropertySources ignoredPropertySources = new IgnoredPropertySources(getPropertyOrDefaultAndTrace(abstractEnvironment, KEY_FOR_SOURCES_IGNORED, Set.class, DEFAULT_SOURCES_IGNORED)); - PropertiesLogger propertiesLogger = new PropertiesLogger(propertiesWithHiddenValues, allowedPrefixForProperties, ignoredPropertySources); - propertiesLogger.doLogProperties(abstractEnvironment); + PropertiesLogger propertiesLogger = new PropertiesLogger(propertiesWithHiddenValues, allowedPrefixForProperties, ignoredPropertySources, abstractEnvironment); + propertiesLogger.doLogProperties(); } private T getPropertyOrDefaultAndTrace(PropertyResolver environment, String key, Class clazz, T defaultValue) { + T result; try { - T result = environment.getProperty(key, clazz, defaultValue); - log.trace(() -> key + " -> " + result); - return result; - } catch (Exception e) { - log.info(()-> "Error while getting property " + key + " : " + e.getMessage()+System.lineSeparator()+"Will use default value"); - return defaultValue; + result = getPropertyOrDefaultAndTraceThrowingException(environment, key, clazz, defaultValue); + } catch (RuntimeException e) { + logExceptionWhenGettingProperty(key, e); + result= defaultValue; } + return result; + } + + private static void logExceptionWhenGettingProperty(String key, RuntimeException e) { + log.info(()-> "Error while getting property " + key + " : " + e.getMessage()+System.lineSeparator()+"Will use default value"); + } + + private static T getPropertyOrDefaultAndTraceThrowingException(PropertyResolver environment, String key, Class clazz, T defaultValue) throws RuntimeException{ + T result = environment.getProperty(key, clazz, defaultValue); + log.trace(() -> key + " -> " + result); + return result; } static final class CustomAbstractEnvironment implements PropertyResolver { @@ -81,6 +92,7 @@ static final class CustomAbstractEnvironment implements PropertyResolver { private static final Method getPropertyAsRawString = resolveMethod(AbstractPropertyResolver.class, "getPropertyAsRawString", String.class); private final ConfigurableEnvironment delegate; + @Nullable private AbstractPropertyResolver propertyResolver; @@ -90,10 +102,11 @@ private static Method resolveMethod(Class targetType, String methodName, Clas return method; } - private CustomAbstractEnvironment(ConfigurableEnvironment delegate) { + CustomAbstractEnvironment(ConfigurableEnvironment delegate) { this.delegate = delegate; } + @Nullable private AbstractPropertyResolver invokeGetPropertyResolver(ConfigurableEnvironment delegate) { if (delegate instanceof AbstractEnvironment abstractEnvironment) { var abstractPropertyResolverCondidate = ReflectionUtils.invokeMethod(getPropertyResolver, abstractEnvironment); @@ -110,7 +123,7 @@ public boolean containsProperty(String key) { } @Override - public String getProperty(String key) { + public @Nullable String getProperty(String key) { return delegate.getProperty(key); } @@ -153,6 +166,7 @@ public MutablePropertySources getPropertySources() { return delegate.getPropertySources(); } + @Nullable public String getPropertySafely(String key) { try{ return delegate.getProperty(key); @@ -164,10 +178,12 @@ public String getPropertySafely(String key) { } } + @Nullable private String getPropertyAsRawString(String key) { return invokeGetPropertyAsRawString(key); } + @Nullable private String invokeGetPropertyAsRawString(String key) { if (this.propertyResolver == null) { this.propertyResolver = invokeGetPropertyResolver(this.delegate); diff --git a/src/main/java/io/github/fbibonne/springaddons/boot/propertieslogger/IgnoredPropertySources.java b/src/main/java/io/github/fbibonne/springaddons/boot/propertieslogger/IgnoredPropertySources.java index 88ac678..eda03e9 100644 --- a/src/main/java/io/github/fbibonne/springaddons/boot/propertieslogger/IgnoredPropertySources.java +++ b/src/main/java/io/github/fbibonne/springaddons/boot/propertieslogger/IgnoredPropertySources.java @@ -1,10 +1,25 @@ package io.github.fbibonne.springaddons.boot.propertieslogger; +import org.springframework.core.env.PropertySource; + import java.util.Set; import java.util.stream.Stream; record IgnoredPropertySources(Set sources) { - public Stream stream() { + private Stream stream() { return sources.stream(); } + + /** + * Return true if the library is configured to ignore the propertySource passed as a parameter. + *
+ * The propertySource must be ignored if its name is listed in the configuration property whose key is + * {@link EnvironmentPreparedEventForPropertiesLogging#KEY_FOR_SOURCES_IGNORED} (default names are + * {@link EnvironmentPreparedEventForPropertiesLogging#DEFAULT_SOURCES_IGNORED} + * @param propertySource : propertySource to be ignored or not + * @return true if propertySource must be ignored + */ + public boolean isIgnored(PropertySource propertySource) { + return stream().anyMatch(propertySource.getName()::contains); + } } diff --git a/src/main/java/io/github/fbibonne/springaddons/boot/propertieslogger/OriginFinder.java b/src/main/java/io/github/fbibonne/springaddons/boot/propertieslogger/OriginFinder.java new file mode 100644 index 0000000..27d7f4f --- /dev/null +++ b/src/main/java/io/github/fbibonne/springaddons/boot/propertieslogger/OriginFinder.java @@ -0,0 +1,37 @@ +package io.github.fbibonne.springaddons.boot.propertieslogger; + +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyNameException; +import org.springframework.core.env.MutablePropertySources; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public record OriginFinder(List configurationPropertySources) { + + public OriginFinder(MutablePropertySources propertySources) { + this(propertySources.stream() + .map(ConfigurationPropertySource::from) + .filter(Objects::nonNull) + .toList()); + } + + public Optional findOriginFor(String key) { + try { + final ConfigurationPropertyName configurationPropertyName = ConfigurationPropertyName.of(key); + return configurationPropertySources.stream() + .map(c -> c.getConfigurationProperty(configurationPropertyName)) + .filter(Objects::nonNull) + .map(ConfigurationProperty::getOrigin) + .filter(Objects::nonNull) + .map(Object::toString) + .map("FROM "::concat) + .findFirst(); + } catch (InvalidConfigurationPropertyNameException e) { + return Optional.of("WARNING ! "+e.getMessage() + " : see org.springframework.boot.context.properties.source.ConfigurationPropertyName"); + } + } +} diff --git a/src/main/java/io/github/fbibonne/springaddons/boot/propertieslogger/PropertiesLogger.java b/src/main/java/io/github/fbibonne/springaddons/boot/propertieslogger/PropertiesLogger.java index b213a78..a808727 100644 --- a/src/main/java/io/github/fbibonne/springaddons/boot/propertieslogger/PropertiesLogger.java +++ b/src/main/java/io/github/fbibonne/springaddons/boot/propertieslogger/PropertiesLogger.java @@ -1,23 +1,17 @@ package io.github.fbibonne.springaddons.boot.propertieslogger; -import org.springframework.boot.ansi.AnsiPropertySource; -import org.springframework.boot.env.RandomValuePropertySource; -import org.springframework.boot.origin.OriginLookup; +import org.jspecify.annotations.Nullable; import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.PropertySource; -import org.springframework.jndi.JndiPropertySource; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; -import java.util.function.Consumer; +import java.util.*; import java.util.function.Predicate; -import java.util.function.UnaryOperator; +import java.util.stream.Collectors; import java.util.stream.Stream; /** * Class doing the real stuff for logging properties. The collect of properties to log, their value and the logging is done - * inside the {@link this#doLogProperties(EnvironmentPreparedEventForPropertiesLogging.CustomAbstractEnvironment)} method. + * inside the {@link this#doLogProperties()} method. * The state of the class stores {@link PropertySource} found, and a reference to an operator providing the origin of a property * which is found while scanning propertySources */ @@ -30,96 +24,144 @@ class PropertiesLogger { final PropertiesWithHiddenValues propertiesWithHiddenValues; final AllowedPrefixForProperties allowedPrefixForProperties; final IgnoredPropertySources ignoredPropertySources; - final Set propertySourceNames = new HashSet<>(); - final StringBuilder stringWithPropertiesToDisplay = new StringBuilder(); + final EnvironmentPreparedEventForPropertiesLogging.CustomAbstractEnvironment abstractEnvironment; + final OriginFinder originFinder; - UnaryOperator originFinder = k->""; - - PropertiesLogger(PropertiesWithHiddenValues propertiesWithHiddenValues, AllowedPrefixForProperties allowedPrefixForProperties, IgnoredPropertySources ignoredPropertySources) { + PropertiesLogger(PropertiesWithHiddenValues propertiesWithHiddenValues, AllowedPrefixForProperties allowedPrefixForProperties, IgnoredPropertySources ignoredPropertySources, EnvironmentPreparedEventForPropertiesLogging.CustomAbstractEnvironment abstractEnvironment) { this.propertiesWithHiddenValues = propertiesWithHiddenValues; this.allowedPrefixForProperties = allowedPrefixForProperties; this.ignoredPropertySources = ignoredPropertySources; + this.abstractEnvironment = abstractEnvironment; + this.originFinder = new OriginFinder(abstractEnvironment.getPropertySources()); } /** * This method : *
    - *
  1. lists all propertySources of the environment and exclude all those which cannot be processed (see method {@link this#isEnumerable(PropertySource)} - * or are listed to be ignored (see property {@code properties.logger.sources-ignored}
  2. + *
  3. lists all propertySources of the environment and exclude all those which won't be processed : + *
      + *
    • propertySources which will be processed must satisfy two conditions : be a subtype of {@link EnumerablePropertySource} AND + * must not be ignored (see {@link IgnoredPropertySources#isIgnored(PropertySource)}
    • + *
    • debug message is logged if the propertySource is ignored
    • + *
    • warn message is logged if the propertySource is a {@link org.springframework.boot.ansi.AnsiPropertySource} (whose processing is not implmented). + * in such case, it will be ignored
    • + + *
    • If propertySource has an unknown type, a warning is logged
    • + *
    + *
  4. *
  5. for each propertySource not excluded, list all property keys then exclude {@code null} keys and non-allowed prefixed ones (see property {@code properties.logger.prefix-for-properties}
  6. *
  7. order distinct keys with alphabetical order (natural order of {@link String}
  8. *
  9. for each key, compute an expression {@code key = value ### FROM value_origin ###} where {@code value} is the value of the key resolved against - * the environment (or masked if key is listed in property {@code properties.logger.with-hidden-values}) and {@code value_origin} is the - * propertySource which contains the value to which the key is resolved (if field {@link this#originFinder} can resolve it)
  10. + * the environment ({@link PropertiesLogger#abstractEnvironment}) or masked if key is listed in property {@code properties.logger.with-hidden-values}. *
  11. log the list of used propertySources to find keys, the ordered list of properties and their values and origin when available
  12. *
* - * @param abstractEnvironment : the environment of the application wrapped as an {@link io.github.fbibonne.springaddons.boot.propertieslogger.EnvironmentPreparedEventForPropertiesLogging.CustomAbstractEnvironment} - * to safely resolve properties with unknown placeholders */ - public void doLogProperties(final EnvironmentPreparedEventForPropertiesLogging.CustomAbstractEnvironment abstractEnvironment) { - log.debug(() -> "Start logging properties with prefix " + allowedPrefixForProperties + " for all properties sources except " + ignoredPropertySources + ". Values masked for properties whose keys contain " + propertiesWithHiddenValues); - - abstractEnvironment.getPropertySources().stream() - .mapMulti(this::asEnumerablePropertySourceIfHasToBeProcessed) - .flatMap(this::toPropertyNames) - .distinct() - .filter(this::nonNullKeyWithPrefix) - .sorted() - .forEach(key -> resolveValueThenAppendToDisplay(key, abstractEnvironment)); - - stringWithPropertiesToDisplay - .append(SEPARATION_LINE) - .insert(0, System.lineSeparator()) - .insert(0, " ===="); - insertPropretySourceNamesOnePerLine(propertySourceNames, stringWithPropertiesToDisplay); - stringWithPropertiesToDisplay.insert(0, """ - - Values of properties from sources : - """ - ) - .insert(0, SEPARATION_LINE) - .insert(0, System.lineSeparator()); + public void doLogProperties() { + debugStarting(); + final StringBuilder stringWithPropertiesToDisplay = new StringBuilder(); + + Map propertyNamesBySource = propertyNamesBySourceFromEnvironment(); + final Set propertySourceNames = propertyNamesBySource.keySet(); + + stringWithPropertiesToDisplay.append(toKeyValuesBlock(propertyNamesBySource)) + .append(System.lineSeparator()) + .append(SEPARATION_LINE); + + stringWithPropertiesToDisplay.insert(0, headerBlock(propertySourceNames)); log.info(stringWithPropertiesToDisplay::toString); } - private void insertPropretySourceNamesOnePerLine(Set propertySourceNames, StringBuilder stringWithPropertiesToDisplay) { - propertySourceNames.forEach(name -> stringWithPropertiesToDisplay.insert(0, System.lineSeparator()).insert(0, name).insert(0, "- ")); + private String headerBlock(Set propertySourceNames) { + return """ + + %1$s + Values of properties from sources : + %2$s + ==== + """.formatted(SEPARATION_LINE, insertPropretySourceNamesOnePerLine(propertySourceNames)); + } + + private String toKeyValuesBlock(Map propertyNamesBySource) { + return distinctPropertiesNames(propertyNamesBySource) + .filter(this::keyWithAllowedPrefix) + .sorted() + .map(this::toDisplayedLine) + .collect(Collectors.joining(System.lineSeparator())); + } + + private static Stream distinctPropertiesNames(Map propertyNamesBySource) { + return propertyNamesBySource.values().stream() + .flatMap(Arrays::stream) + .distinct(); + } + + private Map propertyNamesBySourceFromEnvironment() { + Map propertyNamesBySource = new HashMap<>(); + for (PropertySource propertySource : this.abstractEnvironment.getPropertySources()) { + asEnumerablePropertySourceIfHasToBeProcessed(propertySource) + .ifPresent(enumerablePropertySource -> + propertyNamesBySource.put(enumerablePropertySource.getName(), enumerablePropertySource.getPropertyNames()) + ); + } + return propertyNamesBySource; + } + + private void debugStarting() { + log.debug(() -> "Start logging properties with prefix " + allowedPrefixForProperties + " for all properties sources except " + + ignoredPropertySources + ". Values masked for properties whose keys contain " + propertiesWithHiddenValues); } - private void asEnumerablePropertySourceIfHasToBeProcessed(PropertySource propertySource, Consumer> downstream) { - var mustBeProcessed = isEnumerable(propertySource) && isNotIgnored(propertySource); - if (mustBeProcessed) { - propertySourceNames.add(propertySource.getName()); - downstream.accept((EnumerablePropertySource) propertySource); + private String insertPropretySourceNamesOnePerLine(Set propertySourceNames) { + return propertySourceNames.stream().sorted().map(name -> "- " + name).collect(Collectors.joining(System.lineSeparator())); + } + + private Optional> asEnumerablePropertySourceIfHasToBeProcessed(PropertySource propertySource) { + if (mustBeProcessed(propertySource)) { + return Optional.of((EnumerablePropertySource) propertySource); } + return Optional.empty(); + } + + private boolean mustBeProcessed(PropertySource propertySource) { + return PropertySourceType.isEnumerable(propertySource) && isNotIgnored(propertySource); } private boolean isNotIgnored(PropertySource propertySource) { - if (ignoredPropertySources.stream().anyMatch(propertySource.getName()::contains)) { - log.trace(() -> propertySource + " is listed to be ignored"); + if (ignoredPropertySources.isIgnored(propertySource)) { + traceIgnored(propertySource); return false; } return true; } - private void resolveValueThenAppendToDisplay(String key, EnvironmentPreparedEventForPropertiesLogging.CustomAbstractEnvironment environement) { - stringWithPropertiesToDisplay.append(key).append(" = ") - .append(resolveValueThenMaskItIfSecret(key, environement)) - .append(originFinder.apply(key)) - .append(System.lineSeparator()); + private static void traceIgnored(PropertySource propertySource) { + log.trace(() -> propertySource + " is listed to be ignored"); + } + + private String toDisplayedLine(String key) { + return key + " = " + resolveValueThenMaskItIfSecret(key, this.abstractEnvironment) + + this.originFinder.findOriginFor(key).map(PropertiesLogger::originAsLine).orElse(""); } - private String resolveValueThenMaskItIfSecret(String key, EnvironmentPreparedEventForPropertiesLogging.CustomAbstractEnvironment environment) { - if (propertiesWithHiddenValues.stream().anyMatch(isValueContainedIgnoringCaseIn(key))) { + private static String originAsLine(String origin) { + return " ### " + origin + " ###"; + } + + private @Nullable String resolveValueThenMaskItIfSecret(String key, EnvironmentPreparedEventForPropertiesLogging.CustomAbstractEnvironment environment) { + if (mustBeMasked(key)) { return MASK; } return environment.getPropertySafely(key); } + private boolean mustBeMasked(String key) { + return propertiesWithHiddenValues.stream().anyMatch(isValueContainedIgnoringCaseIn(key)); + } + /** * Implementation of a String#containsIgnoreCase based the algorithm of String#contains * but using String#equalsIgnoreCase to test equality @@ -130,9 +172,6 @@ private String resolveValueThenMaskItIfSecret(String key, EnvironmentPreparedEve */ Predicate isValueContainedIgnoringCaseIn(String container) { return value -> { - if (value==null){ - return false; - } if (value.isEmpty()){ return true; } @@ -146,69 +185,21 @@ Predicate isValueContainedIgnoringCaseIn(String container) { }; } - private Stream toPropertyNames(EnumerablePropertySource propertySource) { - log.trace(() -> "Flat properties for " + propertySource.getName()); - return Arrays.stream(propertySource.getPropertyNames()); - } - - /** - * Method which filter propertySource which can be processed to find properties to log. - * propertySource which can be processed are a subtype of {@link EnumerablePropertySource} : if not method log a - * debug message or a warning message for cases not implemented yet (AnsiPropertySource and JndiPropertySource). If - * propertySource is of type {@link org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertySource}, - * it will be referenced to resolve origin of property values later (see {@link this#originFinder}. If propertySource - * has an unknown type, a warning is logged. - * @param propertySource : instance of {@link PropertySource} which is checked for being processed or not. - * @return true if the propertySource can be processed. - */ - private boolean isEnumerable(PropertySource propertySource) { - if (propertySource instanceof AnsiPropertySource ansiPropertySource) { - log.warn(()->"Processing of AnsiPropertySource "+ ansiPropertySource+" not yet implemented : properties exclusively from this property source will be ignored"); - return false; - } - if (propertySource instanceof PropertySource.StubPropertySource) { - log.debug(() -> propertySource + " is a stub property source : it does not contain properties : will be ignored"); - return false; - } - if (propertySource instanceof RandomValuePropertySource) { - log.debug(()-> propertySource + " is a RandomValuePropertySource : will be ignored"); - return false; - } - if (propertySource instanceof JndiPropertySource){ - log.warn(()-> propertySource + " is a JndiPropertySource : it is not enumerable : will be ignored"); - return false; - } - if (propertySource instanceof EnumerablePropertySource){ - log.trace(() -> propertySource + " is a EnumerablePropertySource : is a candidate to find keys"); - return true; - } - if (propertySource instanceof OriginLookup) { - if ("org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertySource".equals(propertySource.getClass().getCanonicalName())){ - log.debug(()-> propertySource + " will be used as the originFinder but will be ignored as a property source"); - final OriginLookup originLookup = (OriginLookup) propertySource; - this.originFinder=key->{ - var origin=originLookup.getOrigin(key); - return origin==null?"":" ### FROM "+ origin+" ###"; - }; - }else{ - log.debug(()-> propertySource + " will be ignored as a property source and also as OriginLookup"); - } - return false; + private boolean keyWithAllowedPrefix(String key) { + log.trace(() -> "Check if property " + key + " can be displayed"); + boolean keyWithAllowedPrefix = isKeyWithAllowedPrefix(key); + if (!keyWithAllowedPrefix) { + debugNotAllowedPrefix(key); } - log.warn(()-> propertySource + " is unknown : will be ignored"); - return false; + return keyWithAllowedPrefix; } - private boolean nonNullKeyWithPrefix(String key) { - log.trace(() -> "Check if property " + key + " can be displayed"); - if (key == null) { - return false; - } - if (allowedPrefixForProperties.anyMatch(key::startsWith)) { - return true; - } + private static void debugNotAllowedPrefix(String key) { log.debug(() -> key + " doesn't start with a logable prefix"); - return false; + } + + private boolean isKeyWithAllowedPrefix(String key) { + return allowedPrefixForProperties.anyMatch(key::startsWith); } } diff --git a/src/main/java/io/github/fbibonne/springaddons/boot/propertieslogger/PropertySourceType.java b/src/main/java/io/github/fbibonne/springaddons/boot/propertieslogger/PropertySourceType.java new file mode 100644 index 0000000..9d8e001 --- /dev/null +++ b/src/main/java/io/github/fbibonne/springaddons/boot/propertieslogger/PropertySourceType.java @@ -0,0 +1,92 @@ +package io.github.fbibonne.springaddons.boot.propertieslogger; + +import org.springframework.boot.ansi.AnsiPropertySource; +import org.springframework.boot.env.RandomValuePropertySource; +import org.springframework.boot.origin.OriginLookup; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.jndi.JndiPropertySource; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +public enum PropertySourceType { + ANSI_PROPERTY_SOURCE, + JDNI_PROPERTY_SOURCE_TYPE, + ENUMERABLE_PROPERTY_SOURCE, + OTHERS, UNKNOWN; + + private static final LocalLogger log = new LocalLogger(PropertySourceType.class); + + public static boolean isEnumerable(PropertySource propertySource) { + PropertySourceType propertySourceType = of(propertySource); + propertySourceType.log(propertySource); + return propertySourceType.isEnumerable(); + } + + private static PropertySourceType of(PropertySource propertySource) { + return isEnumerableInstance(propertySource) ? ENUMERABLE_PROPERTY_SOURCE : ofNonEnumerablePropertySource(propertySource); + } + + private static PropertySourceType ofNonEnumerablePropertySource(PropertySource propertySource) { + PropertySourceType propertySourceType; + if (isAnsiPropertySource(propertySource)){ + propertySourceType = ANSI_PROPERTY_SOURCE; + } else if (propertySource instanceof JndiPropertySource) { + propertySourceType = JDNI_PROPERTY_SOURCE_TYPE; + }else { + propertySourceType = ofOtherPropertySource(propertySource); + } + return propertySourceType; + } + + private static PropertySourceType ofOtherPropertySource(PropertySource propertySource) { + PropertySourceType propertySourceType; + if (isOfOtherInstance(propertySource)) { + propertySourceType = OTHERS; + } else { + propertySourceType = UNKNOWN; + } + return propertySourceType; + } + + private static boolean isOfOtherInstance(PropertySource propertySource) { + return propertySource instanceof PropertySource.StubPropertySource + || propertySource instanceof RandomValuePropertySource + || propertySource instanceof OriginLookup; + } + + private static boolean isAnsiPropertySource(PropertySource propertySource) { + return propertySource instanceof AnsiPropertySource; + } + + private static boolean isEnumerableInstance(PropertySource propertySource) { + return propertySource instanceof EnumerablePropertySource; + } + + private void log(PropertySource propertySource) { + withLogLevel().accept(() -> logMessage(propertySource)); + } + + private String logMessage(PropertySource propertySource) { + return switch (this) { + case ANSI_PROPERTY_SOURCE -> + "Processing of AnsiPropertySource " + propertySource + " not yet implemented : properties exclusively from this property source will be ignored"; + case OTHERS, JDNI_PROPERTY_SOURCE_TYPE, UNKNOWN -> propertySource + " is not enumerable : will be ignored"; + case ENUMERABLE_PROPERTY_SOURCE -> + propertySource + " is a EnumerablePropertySource : is a candidate to find keys"; + }; + } + + private Consumer> withLogLevel() { + return switch (this) { + case ANSI_PROPERTY_SOURCE, JDNI_PROPERTY_SOURCE_TYPE, UNKNOWN -> log::warn; + case OTHERS -> log::debug; + case ENUMERABLE_PROPERTY_SOURCE -> log::trace; + }; + } + + private boolean isEnumerable() { + return this == ENUMERABLE_PROPERTY_SOURCE; + } +} diff --git a/src/main/java/io/github/fbibonne/springaddons/boot/propertieslogger/package-info.java b/src/main/java/io/github/fbibonne/springaddons/boot/propertieslogger/package-info.java new file mode 100644 index 0000000..2c509e9 --- /dev/null +++ b/src/main/java/io/github/fbibonne/springaddons/boot/propertieslogger/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package io.github.fbibonne.springaddons.boot.propertieslogger; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/src/test/java/io/github/fbibonne/springaddons/boot/propertieslogger/PropertiesLoggerTest.java b/src/test/java/io/github/fbibonne/springaddons/boot/propertieslogger/PropertiesLoggerTest.java index 668de58..853ecfe 100644 --- a/src/test/java/io/github/fbibonne/springaddons/boot/propertieslogger/PropertiesLoggerTest.java +++ b/src/test/java/io/github/fbibonne/springaddons/boot/propertieslogger/PropertiesLoggerTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.mock.env.MockEnvironment; import static org.assertj.core.api.Assertions.assertThat; @@ -26,14 +27,15 @@ class PropertiesLoggerTest { "'stringFirst', 'StrinG', true", "'stringLast', 'Lasts', false", "'stringFirst', 'astring', false", - "'null.is.not.contained', , false", "'empty.is.contained', '', true", "'self.is.contained', 'self.is.contained', true", "'self.is.contained', 'SelF.iS.conTained', true", "'longer.is.not.contained','longer.is.not.contained+', false" }) void isValueContainedIgnoringCaseInTest(String container, String value, boolean expected) { - PropertiesLogger propertiesLogger = new PropertiesLogger(null, null, null); + PropertiesLogger propertiesLogger = new PropertiesLogger(null, null, null, + new EnvironmentPreparedEventForPropertiesLogging.CustomAbstractEnvironment(new MockEnvironment()) + ); assertThat(propertiesLogger.isValueContainedIgnoringCaseIn(container).test(value)).isEqualTo(expected); } } \ No newline at end of file diff --git a/src/test/java/io/github/fbibonne/test/WebAppIntegrationTest.java b/src/test/java/io/github/fbibonne/test/WebAppIntegrationTest.java index 71bd93d..5370854 100644 --- a/src/test/java/io/github/fbibonne/test/WebAppIntegrationTest.java +++ b/src/test/java/io/github/fbibonne/test/WebAppIntegrationTest.java @@ -28,24 +28,24 @@ class WebAppIntegrationTest { @Test @DisplayName("Properties should be correctly displayed and sorted when starting in Web context") void contextLoads(CapturedOutput output) { - // ### FROM "spring.application.pid" from property source "applicationInfo" ### + //### FROM "spring.application.pid" from property source "applicationInfo" ### assertThat(output.toString()).contains((""" main] i.g.f.s.b.p.PropertiesLogger : %n\ ================================================================================ Values of properties from sources : + - Config resource 'class path resource [additional-file.properties]' via location 'classpath:additional-file.properties'%n\ - Config resource 'class path resource [application.properties]' via location 'optional:classpath:/'%n\ - Config resource 'file [src/test/resources/otherProps/application.properties]' via location 'file:src/test/resources/otherProps/application.properties'%n\ + - Inlined Test Properties%n\ - applicationInfo%n\ - - Config resource 'class path resource [additional-file.properties]' via location 'classpath:additional-file.properties'%n\ - commandLineArgs%n\ - - Inlined Test Properties%n\ - systemProperties%n\ ====%n\ from.system.properties = true ### FROM "from.system.properties" from property source "systemProperties" ### io.github.fbibonne.secret = ****** ### FROM class path resource [application.properties] - 2:29 ### io.github.fbibonne.shared = additionalPropsInClasspath ### FROM URL [file:src/test/resources/otherProps/application.properties] - 1:29 ### io.github.fbibonne.specific.additional-file = additional-file.properties ### FROM class path resource [additional-file.properties] - 2:47 ### - io.github.fbibonne.specific.additionalPropsInClasspath = additionalPropsInClasspath + io.github.fbibonne.specific.additionalPropsInClasspath = additionalPropsInClasspath ### WARNING ! Configuration property name 'io.github.fbibonne.specific.additionalPropsInClasspath' is not valid : see org.springframework.boot.context.properties.source.ConfigurationPropertyName ### io.github.fbibonne.specific.applicationproperties = application.properties ### FROM class path resource [application.properties] - 4:53 ### io.github.fbibonne.test = ok ### FROM class path resource [application.properties] - 1:27 ### logging.level.io.github.fbibonne.springaddons.boot = trace ### FROM "logging.level.io.github.fbibonne.springaddons.boot" from property source "Inlined Test Properties" ###