This is an automated email from the ASF dual-hosted git repository. pkarwasz pushed a commit to branch fix/1963_add_logging_to_properties_util in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit ba539356d1b555ee52ddcc7c29be11d5a862ad79 Author: Piotr P. Karwasz <[email protected]> AuthorDate: Thu Mar 28 13:39:53 2024 +0100 Add logging to `PropertiesUtil` and fix `Duration` parser --- .../logging/log4j/util/PropertiesUtilTest.java | 67 ++++++++++ .../log4j/util/EnvironmentPropertySource.java | 30 +++-- .../apache/logging/log4j/util/PropertiesUtil.java | 139 +++++++++++++-------- .../org/apache/logging/log4j/util/Strings.java | 3 +- .../log4j/util/SystemPropertiesPropertySource.java | 43 ++++--- .../.2.x.x/1963_add_logging_to_properties_util.xml | 8 ++ 6 files changed, 213 insertions(+), 77 deletions(-) diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/util/PropertiesUtilTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/util/PropertiesUtilTest.java index 6e3600eafe..07fd8c85e8 100644 --- a/log4j-api-test/src/test/java/org/apache/logging/log4j/util/PropertiesUtilTest.java +++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/util/PropertiesUtilTest.java @@ -16,19 +16,36 @@ */ package org.apache.logging.log4j.util; +import static java.time.temporal.ChronoUnit.DAYS; +import static java.time.temporal.ChronoUnit.HOURS; +import static java.time.temporal.ChronoUnit.MICROS; +import static java.time.temporal.ChronoUnit.MILLIS; +import static java.time.temporal.ChronoUnit.MINUTES; +import static java.time.temporal.ChronoUnit.NANOS; +import static java.time.temporal.ChronoUnit.SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.format.DateTimeParseException; +import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.ResourceAccessMode; import org.junit.jupiter.api.parallel.ResourceLock; import org.junit.jupiter.api.parallel.Resources; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junitpioneer.jupiter.ReadsSystemProperty; public class PropertiesUtilTest { @@ -80,6 +97,56 @@ public class PropertiesUtilTest { assertEquals(Charset.defaultCharset(), pu.getCharsetProperty("e.2")); } + static Stream<Arguments> should_properly_parse_duration() { + return Stream.of( + Arguments.of(Duration.of(1, NANOS), "1 ns"), + Arguments.of(Duration.of(1, NANOS), "1 nano"), + Arguments.of(Duration.of(3, NANOS), "3 nanos"), + Arguments.of(Duration.of(1, NANOS), "1 nanosecond"), + Arguments.of(Duration.of(5, NANOS), "5 nanoseconds"), + Arguments.of(Duration.of(6, MICROS), "6 us"), + Arguments.of(Duration.of(1, MICROS), "1 micro"), + Arguments.of(Duration.of(8, MICROS), "8 micros"), + Arguments.of(Duration.of(1, MICROS), "1 microsecond"), + Arguments.of(Duration.of(10, MICROS), "10 microseconds"), + Arguments.of(Duration.of(11, MILLIS), "11 ms"), + Arguments.of(Duration.of(1, MILLIS), "1 milli"), + Arguments.of(Duration.of(13, MILLIS), "13 millis"), + Arguments.of(Duration.of(1, MILLIS), "1 millisecond"), + Arguments.of(Duration.of(15, MILLIS), "15 milliseconds"), + Arguments.of(Duration.of(16, SECONDS), "16 s"), + Arguments.of(Duration.of(1, SECONDS), "1 second"), + Arguments.of(Duration.of(18, SECONDS), "18 seconds"), + Arguments.of(Duration.of(19, MINUTES), "19 m"), + Arguments.of(Duration.of(1, MINUTES), "1 minute"), + Arguments.of(Duration.of(21, MINUTES), "21 minutes"), + Arguments.of(Duration.of(22, HOURS), "22 h"), + Arguments.of(Duration.of(1, HOURS), "1 hour"), + Arguments.of(Duration.of(24, HOURS), "24 hours"), + Arguments.of(Duration.of(25, DAYS), "25 d"), + Arguments.of(Duration.of(1, DAYS), "1 day"), + Arguments.of(Duration.of(27, DAYS), "27 days"), + Arguments.of(Duration.of(28, MILLIS), "28")); + } + + @ParameterizedTest + @MethodSource + void should_properly_parse_duration(final Duration expected, final String value) { + Assertions.assertThat(PropertiesUtil.parseDuration(value)).isEqualTo(expected); + } + + static List<String> should_throw_on_invalid_duration() { + return Arrays.asList( + // more than long + "18446744073709551616 nanos", "1 month", "invalid pattern"); + } + + @ParameterizedTest + @MethodSource + void should_throw_on_invalid_duration(final String value) { + assertThrows(DateTimeParseException.class, () -> PropertiesUtil.parseDuration(value)); + } + @Test @ResourceLock(value = Resources.SYSTEM_PROPERTIES, mode = ResourceAccessMode.READ) public void testGetMappedProperty_sun_stdout_encoding() { diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/EnvironmentPropertySource.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/EnvironmentPropertySource.java index dc73c5056f..c9edf087d8 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/util/EnvironmentPropertySource.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/EnvironmentPropertySource.java @@ -36,19 +36,31 @@ import org.apache.logging.log4j.status.StatusLogger; public class EnvironmentPropertySource implements PropertySource { private static final Logger LOGGER = StatusLogger.getLogger(); - private static final String PREFIX = "LOG4J_"; - private static final int DEFAULT_PRIORITY = 100; + private static final PropertySource INSTANCE = new EnvironmentPropertySource(); + + /** + * Method used by Java 9+ to instantiate providers + * @since 2.24.0 + * @see java.util.ServiceLoader + */ + public static PropertySource provider() { + return INSTANCE; + } + @Override public int getPriority() { return DEFAULT_PRIORITY; } - private void logException(final SecurityException error) { - // There is no status logger yet. - LOGGER.error("The system environment variables are not available to Log4j due to security restrictions", error); + private static void logException(final SecurityException error) { + LOGGER.error("The environment variables are not available to Log4j due to security restrictions.", error); + } + + private static void logException(final SecurityException error, final String key) { + LOGGER.error("The environment variable {} is not available to Log4j due to security restrictions.", key, error); } @Override @@ -88,8 +100,8 @@ public class EnvironmentPropertySource implements PropertySource { return System.getenv().keySet(); } catch (final SecurityException e) { logException(e); - return PropertySource.super.getPropertyNames(); } + return PropertySource.super.getPropertyNames(); } @Override @@ -97,9 +109,9 @@ public class EnvironmentPropertySource implements PropertySource { try { return System.getenv(key); } catch (final SecurityException e) { - logException(e); - return PropertySource.super.getProperty(key); + logException(e, key); } + return PropertySource.super.getProperty(key); } @Override @@ -107,7 +119,7 @@ public class EnvironmentPropertySource implements PropertySource { try { return System.getenv().containsKey(key); } catch (final SecurityException e) { - logException(e); + logException(e, key); return PropertySource.super.containsProperty(key); } } diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertiesUtil.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertiesUtil.java index 310f432edd..9865dd364d 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertiesUtil.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertiesUtil.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import java.time.Duration; +import java.time.format.DateTimeParseException; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalUnit; import java.util.ArrayList; @@ -36,6 +37,9 @@ import java.util.ServiceLoader; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListSet; +import java.util.regex.MatchResult; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.status.StatusLogger; @@ -64,6 +68,8 @@ public final class PropertiesUtil { private static final Lazy<PropertiesUtil> COMPONENT_PROPERTIES = Lazy.lazy(() -> new PropertiesUtil(LOG4J_PROPERTIES_FILE_NAME, false)); + private static final Pattern DURATION_PATTERN = Pattern.compile("([+-]?\\d+)\\s*(\\w+)?", Pattern.CASE_INSENSITIVE); + private final Environment environment; /** @@ -200,9 +206,8 @@ public final class PropertiesUtil { * @return The value or null if it is not found. * @since 2.13.0 */ - @SuppressWarnings("deprecation") public Boolean getBooleanProperty(final String[] prefixes, final String key, final Supplier<Boolean> supplier) { - for (String prefix : prefixes) { + for (final String prefix : prefixes) { if (hasProperty(prefix + key)) { return getBooleanProperty(prefix + key); } @@ -243,7 +248,7 @@ public final class PropertiesUtil { return Charset.forName(mapped); } } - LOGGER.error( + LOGGER.warn( "Unable to read charset `{}` from property `{}`. Falling back to the default: `{}`", charsetName, name, @@ -263,8 +268,13 @@ public final class PropertiesUtil { if (prop != null) { try { return Double.parseDouble(prop); - } catch (final Exception ignored) { - // returns default value + } catch (final NumberFormatException e) { + LOGGER.warn( + "Unable to read double `{}` from property `{}`. Falling back to the default: `{}`", + prop, + name, + defaultValue, + e); } } return defaultValue; @@ -283,8 +293,13 @@ public final class PropertiesUtil { if (prop != null) { try { return Integer.parseInt(prop.trim()); - } catch (final Exception ignored) { - // ignore + } catch (final NumberFormatException e) { + LOGGER.warn( + "Unable to read int `{}` from property `{}`. Falling back to the default: `{}`", + prop, + name, + defaultValue, + e); } } return defaultValue; @@ -299,9 +314,8 @@ public final class PropertiesUtil { * @return The value or null if it is not found. * @since 2.13.0 */ - @SuppressWarnings("deprecation") public Integer getIntegerProperty(final String[] prefixes, final String key, final Supplier<Integer> supplier) { - for (String prefix : prefixes) { + for (final String prefix : prefixes) { if (hasProperty(prefix + key)) { return getIntegerProperty(prefix + key, 0); } @@ -321,8 +335,13 @@ public final class PropertiesUtil { if (prop != null) { try { return Long.parseLong(prop); - } catch (final Exception ignored) { - // returns the default value + } catch (final NumberFormatException e) { + LOGGER.warn( + "Unable to read long `{}` from property `{}`. Falling back to the default: `{}`", + prop, + name, + defaultValue, + e); } } return defaultValue; @@ -337,9 +356,8 @@ public final class PropertiesUtil { * @return The value or null if it is not found. * @since 2.13.0 */ - @SuppressWarnings("deprecation") public Long getLongProperty(final String[] prefixes, final String key, final Supplier<Long> supplier) { - for (String prefix : prefixes) { + for (final String prefix : prefixes) { if (hasProperty(prefix + key)) { return getLongProperty(prefix + key, 0); } @@ -357,8 +375,15 @@ public final class PropertiesUtil { */ public Duration getDurationProperty(final String name, final Duration defaultValue) { final String prop = getStringProperty(name); - if (prop != null) { - return TimeUnit.getDuration(prop); + try { + return parseDuration(prop); + } catch (final DateTimeParseException e) { + LOGGER.warn( + "Unable to read duration `{}` from property `{}`. Falling back to the default: `{}`", + prop, + name, + defaultValue, + e); } return defaultValue; } @@ -372,9 +397,8 @@ public final class PropertiesUtil { * @return The value or null if it is not found. * @since 2.13.0 */ - @SuppressWarnings("deprecation") public Duration getDurationProperty(final String[] prefixes, final String key, final Supplier<Duration> supplier) { - for (String prefix : prefixes) { + for (final String prefix : prefixes) { if (hasProperty(prefix + key)) { return getDurationProperty(prefix + key, null); } @@ -391,9 +415,8 @@ public final class PropertiesUtil { * @return The value or null if it is not found. * @since 2.13.0 */ - @SuppressWarnings("deprecation") public String getStringProperty(final String[] prefixes, final String key, final Supplier<String> supplier) { - for (String prefix : prefixes) { + for (final String prefix : prefixes) { final String result = getStringProperty(prefix + key); if (result != null) { return result; @@ -472,16 +495,18 @@ public final class PropertiesUtil { private final Map<List<CharSequence>, String> tokenized = new ConcurrentHashMap<>(); private Environment(final PropertySource propertySource) { - final PropertyFilePropertySource sysProps = - new PropertyFilePropertySource(LOG4J_SYSTEM_PROPERTIES_FILE_NAME, false); + final PropertySource sysProps = new PropertyFilePropertySource(LOG4J_SYSTEM_PROPERTIES_FILE_NAME, false); try { sysProps.forEach((key, value) -> { if (System.getProperty(key) == null) { System.setProperty(key, value); } }); - } catch (SecurityException ex) { - // Access to System Properties is restricted so just skip it. + } catch (final SecurityException e) { + LOGGER.warn( + "Unable to set Java system properties from {} file, due to security restrictions.", + LOG4J_SYSTEM_PROPERTIES_FILE_NAME, + e); } sources.add(propertySource); // We don't log anything on the status logger. @@ -649,45 +674,57 @@ public final class PropertiesUtil { * @return true if system properties tell us we are running on Windows. */ public boolean isOsWindows() { - return getStringProperty("os.name", "").startsWith("Windows"); + return SystemPropertiesPropertySource.getSystemProperty("os.name", "").startsWith("Windows"); + } + + static Duration parseDuration(final String value) { + final Matcher matcher = DURATION_PATTERN.matcher(value); + if (matcher.matches()) { + return Duration.of(TimeUnit.parseAmount(matcher), TimeUnit.parseUnit(matcher)); + } + throw new DateTimeParseException("Invalid duration value: should be in the format nnn[unit].", value, 0); } private enum TimeUnit { - NANOS("ns,nano,nanos,nanosecond,nanoseconds", ChronoUnit.NANOS), - MICROS("us,micro,micros,microsecond,microseconds", ChronoUnit.MICROS), - MILLIS("ms,milli,millis,millsecond,milliseconds", ChronoUnit.MILLIS), - SECONDS("s,second,seconds", ChronoUnit.SECONDS), - MINUTES("m,minute,minutes", ChronoUnit.MINUTES), - HOURS("h,hour,hours", ChronoUnit.HOURS), - DAYS("d,day,days", ChronoUnit.DAYS); - - /* - * https://errorprone.info/bugpattern/ImmutableEnumChecker - * This field is effectively immutable. - */ - @SuppressWarnings("ImmutableEnumChecker") - private final String[] descriptions; + NANOS(new String[] {"ns", "nano", "nanos", "nanosecond", "nanoseconds"}, ChronoUnit.NANOS), + MICROS(new String[] {"us", "micro", "micros", "microsecond", "microseconds"}, ChronoUnit.MICROS), + MILLIS(new String[] {"ms", "milli", "millis", "millisecond", "milliseconds"}, ChronoUnit.MILLIS), + SECONDS(new String[] {"s", "second", "seconds"}, ChronoUnit.SECONDS), + MINUTES(new String[] {"m", "minute", "minutes"}, ChronoUnit.MINUTES), + HOURS(new String[] {"h", "hour", "hours"}, ChronoUnit.HOURS), + DAYS(new String[] {"d", "day", "days"}, ChronoUnit.DAYS); - private final ChronoUnit timeUnit; + private final String[] descriptions; + private final TemporalUnit timeUnit; - TimeUnit(final String descriptions, final ChronoUnit timeUnit) { - this.descriptions = descriptions.split(","); + TimeUnit(final String[] descriptions, final TemporalUnit timeUnit) { + this.descriptions = descriptions; this.timeUnit = timeUnit; } - static Duration getDuration(final String time) { - final String value = time.trim(); - TemporalUnit temporalUnit = ChronoUnit.MILLIS; - long timeVal = 0; - for (TimeUnit timeUnit : values()) { - for (String suffix : timeUnit.descriptions) { - if (value.endsWith(suffix)) { - temporalUnit = timeUnit.timeUnit; - timeVal = Long.parseLong(value.substring(0, value.length() - suffix.length())); + private static long parseAmount(final MatchResult matcher) { + final String amount = matcher.group(1); + try { + return Long.parseLong(amount); + } catch (final NumberFormatException e) { + throw new DateTimeParseException( + "Invalid time amount '" + amount + "'", matcher.group(), matcher.start(1), e); + } + } + + private static TemporalUnit parseUnit(final MatchResult matcher) { + final String unit = matcher.group(2); + if (unit != null) { + for (final TimeUnit value : values()) { + for (final String description : value.descriptions) { + if (unit.equals(description)) { + return value.timeUnit; + } } } + throw new DateTimeParseException("Invalid time unit '" + unit + "'", matcher.group(), matcher.start(2)); } - return Duration.of(timeVal, temporalUnit); + return ChronoUnit.MILLIS; } } } diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java index bee54324f4..91057b53d6 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java @@ -52,8 +52,7 @@ public final class Strings { * OS-dependent line separator, defaults to {@code "\n"} if the system property {@code ""line.separator"} cannot be * read. */ - public static final String LINE_SEPARATOR = - SystemPropertiesPropertySource.getSystemProperty("line.separator", "\n"); + public static final String LINE_SEPARATOR = System.lineSeparator(); /** * Returns a double quoted string. diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/SystemPropertiesPropertySource.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/SystemPropertiesPropertySource.java index c7b04ae933..e49dad0dd5 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/util/SystemPropertiesPropertySource.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/SystemPropertiesPropertySource.java @@ -21,6 +21,8 @@ import aQute.bnd.annotation.spi.ServiceProvider; import java.util.Collection; import java.util.Objects; import java.util.Properties; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.status.StatusLogger; /** * PropertySource backed by the current system properties. Other than having a @@ -32,19 +34,32 @@ import java.util.Properties; @ServiceProvider(value = PropertySource.class, resolution = Resolution.OPTIONAL) public class SystemPropertiesPropertySource implements PropertySource { + private static final Logger LOGGER = StatusLogger.getLogger(); private static final int DEFAULT_PRIORITY = 0; private static final String PREFIX = "log4j2."; + private static final PropertySource INSTANCE = new SystemPropertiesPropertySource(); + /** - * Used by bootstrap code to get system properties without loading PropertiesUtil. + * Method used by Java 9+ to instantiate providers + * @since 2.24.0 + * @see java.util.ServiceLoader */ + public static PropertySource provider() { + return INSTANCE; + } + public static String getSystemProperty(final String key, final String defaultValue) { - try { - return System.getProperty(key, defaultValue); - } catch (SecurityException e) { - // Silently ignore the exception - return defaultValue; - } + final String value = INSTANCE.getProperty(key); + return value != null ? value : defaultValue; + } + + private static void logException(final SecurityException error) { + LOGGER.error("The Java system properties are not available to Log4j due to security restrictions.", error); + } + + private static void logException(final SecurityException error, final String key) { + LOGGER.error("The Java system property {} is not available to Log4j due to security restrictions.", key, error); } @Override @@ -54,15 +69,11 @@ public class SystemPropertiesPropertySource implements PropertySource { @Override public void forEach(final BiConsumer<String, String> action) { - Properties properties; + final Properties properties; try { properties = System.getProperties(); } catch (final SecurityException e) { - // (1) There is no status logger. - // (2) LowLevelLogUtil also consults system properties ("line.separator") to - // open a BufferedWriter, so this may fail as well. Just having a hard reference - // in this code to LowLevelLogUtil would cause a problem. - // (3) We could log to System.err (nah) or just be quiet as we do now. + logException(e); return; } // Lock properties only long enough to get a thread-safe SAFE snapshot of its @@ -89,8 +100,9 @@ public class SystemPropertiesPropertySource implements PropertySource { try { return System.getProperties().stringPropertyNames(); } catch (final SecurityException e) { - return PropertySource.super.getPropertyNames(); + logException(e); } + return PropertySource.super.getPropertyNames(); } @Override @@ -98,8 +110,9 @@ public class SystemPropertiesPropertySource implements PropertySource { try { return System.getProperty(key); } catch (final SecurityException e) { - return PropertySource.super.getProperty(key); + logException(e, key); } + return PropertySource.super.getProperty(key); } @Override diff --git a/src/changelog/.2.x.x/1963_add_logging_to_properties_util.xml b/src/changelog/.2.x.x/1963_add_logging_to_properties_util.xml new file mode 100644 index 0000000000..5745caf8e1 --- /dev/null +++ b/src/changelog/.2.x.x/1963_add_logging_to_properties_util.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="http://logging.apache.org/log4j/changelog" + xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.3.xsd" + type="changed"> + <issue id="1936" link="https://github.com/apache/logging-log4j2/pull/1936"/> + <description format="asciidoc">Add logging to `PropertiesUtil` and fix `Duration` parser.</description> +</entry>
