Skip to content

Commit af045db

Browse files
authored
Fix StatusLogger time-zone issues and stack overflow (#2322)
* Add time-zone support to `StatusLogger`. Without a time-zone, it is not possible to format certain date & time fields that are time-zone-specific, e.g., year-of-era. * Make sure `StatusLogger#logMessage()` failures don't cause stack overflow
1 parent 6d407b6 commit af045db

10 files changed

+640
-39
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.logging.log4j.status;
18+
19+
import static org.apache.logging.log4j.status.StatusLogger.DEFAULT_FALLBACK_LISTENER_BUFFER_CAPACITY;
20+
import static org.assertj.core.api.Assertions.assertThat;
21+
22+
import java.util.Properties;
23+
import org.junit.jupiter.api.Test;
24+
import uk.org.webcompere.systemstubs.SystemStubs;
25+
26+
class StatusLoggerBufferCapacityTest {
27+
28+
@Test
29+
void valid_buffer_capacity_should_be_effective() {
30+
31+
// Create a `StatusLogger` configuration
32+
final Properties statusLoggerConfigProperties = new Properties();
33+
final int bufferCapacity = 10;
34+
assertThat(bufferCapacity).isNotEqualTo(DEFAULT_FALLBACK_LISTENER_BUFFER_CAPACITY);
35+
statusLoggerConfigProperties.put(StatusLogger.MAX_STATUS_ENTRIES, "" + bufferCapacity);
36+
final StatusLogger.Config statusLoggerConfig = new StatusLogger.Config(statusLoggerConfigProperties);
37+
38+
// Verify the buffer capacity
39+
assertThat(statusLoggerConfig.bufferCapacity).isEqualTo(bufferCapacity);
40+
}
41+
42+
@Test
43+
void invalid_buffer_capacity_should_cause_fallback_to_defaults() throws Exception {
44+
45+
// Create a `StatusLogger` configuration using an invalid buffer capacity
46+
final Properties statusLoggerConfigProperties = new Properties();
47+
final int invalidBufferCapacity = -10;
48+
statusLoggerConfigProperties.put(StatusLogger.MAX_STATUS_ENTRIES, "" + invalidBufferCapacity);
49+
final StatusLogger.Config[] statusLoggerConfigRef = {null};
50+
final String stderr = SystemStubs.tapSystemErr(
51+
() -> statusLoggerConfigRef[0] = new StatusLogger.Config(statusLoggerConfigProperties));
52+
final StatusLogger.Config statusLoggerConfig = statusLoggerConfigRef[0];
53+
54+
// Verify the stderr dump
55+
assertThat(stderr).contains("Failed reading the buffer capacity");
56+
57+
// Verify the buffer capacity
58+
assertThat(statusLoggerConfig.bufferCapacity).isEqualTo(DEFAULT_FALLBACK_LISTENER_BUFFER_CAPACITY);
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.logging.log4j.status;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
21+
import edu.umd.cs.findbugs.annotations.Nullable;
22+
import java.time.Instant;
23+
import java.time.ZoneId;
24+
import java.time.format.DateTimeFormatter;
25+
import java.util.Properties;
26+
import org.junit.jupiter.api.Test;
27+
import org.junit.jupiter.params.ParameterizedTest;
28+
import org.junit.jupiter.params.provider.CsvSource;
29+
import uk.org.webcompere.systemstubs.SystemStubs;
30+
31+
class StatusLoggerDateTest {
32+
33+
@ParameterizedTest
34+
@CsvSource({"yyyy-MM-dd", "HH:mm:ss", "HH:mm:ss.SSS"})
35+
void common_date_patterns_should_work(final String instantPattern) {
36+
37+
// Create a `StatusLogger` configuration
38+
final Properties statusLoggerConfigProperties = new Properties();
39+
statusLoggerConfigProperties.put(StatusLogger.STATUS_DATE_FORMAT, instantPattern);
40+
final ZoneId zoneId = ZoneId.of("UTC");
41+
statusLoggerConfigProperties.put(StatusLogger.STATUS_DATE_FORMAT_ZONE, zoneId.toString());
42+
final StatusLogger.Config statusLoggerConfig = new StatusLogger.Config(statusLoggerConfigProperties);
43+
44+
// Verify the formatter
45+
final DateTimeFormatter formatter =
46+
DateTimeFormatter.ofPattern(instantPattern).withZone(zoneId);
47+
verifyFormatter(statusLoggerConfig.instantFormatter, formatter);
48+
}
49+
50+
@Test
51+
void invalid_date_format_should_cause_fallback_to_defaults() throws Exception {
52+
final String invalidFormat = "l";
53+
verifyInvalidDateFormatAndZone(invalidFormat, "UTC", "failed reading the instant format", null);
54+
}
55+
56+
@Test
57+
void invalid_date_format_zone_should_cause_fallback_to_defaults() throws Exception {
58+
final String invalidZone = "XXX";
59+
final String format = "yyyy";
60+
verifyInvalidDateFormatAndZone(
61+
format,
62+
invalidZone,
63+
"Failed reading the instant formatting zone ID",
64+
DateTimeFormatter.ofPattern(format).withZone(ZoneId.systemDefault()));
65+
}
66+
67+
private static void verifyInvalidDateFormatAndZone(
68+
final String format,
69+
final String zone,
70+
final String stderrMessage,
71+
@Nullable final DateTimeFormatter formatter)
72+
throws Exception {
73+
74+
// Create a `StatusLogger` configuration using invalid input
75+
final Properties statusLoggerConfigProperties = new Properties();
76+
statusLoggerConfigProperties.put(StatusLogger.STATUS_DATE_FORMAT, format);
77+
statusLoggerConfigProperties.put(StatusLogger.STATUS_DATE_FORMAT_ZONE, zone);
78+
final StatusLogger.Config[] statusLoggerConfigRef = {null};
79+
final String stderr = SystemStubs.tapSystemErr(
80+
() -> statusLoggerConfigRef[0] = new StatusLogger.Config(statusLoggerConfigProperties));
81+
final StatusLogger.Config statusLoggerConfig = statusLoggerConfigRef[0];
82+
83+
// Verify the stderr dump
84+
assertThat(stderr).contains(stderrMessage);
85+
86+
// Verify the formatter
87+
verifyFormatter(statusLoggerConfig.instantFormatter, formatter);
88+
}
89+
90+
/**
91+
* {@link DateTimeFormatter} doesn't have an {@link Object#equals(Object)} implementation, hence <a href="https://stackoverflow.com/a/63887712/1278899">this manual <em>behavioral</em> comparison</a>.
92+
*
93+
* @param actual the actual formatter
94+
* @param expected the expected formatter
95+
*/
96+
private static void verifyFormatter(@Nullable DateTimeFormatter actual, @Nullable DateTimeFormatter expected) {
97+
if (expected == null) {
98+
assertThat(actual).isNull();
99+
} else {
100+
assertThat(actual).isNotNull();
101+
final Instant instant = Instant.now();
102+
assertThat(actual.format(instant)).isEqualTo(expected.format(instant));
103+
}
104+
}
105+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.logging.log4j.status;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.mockito.ArgumentMatchers.any;
21+
import static org.mockito.Mockito.doThrow;
22+
import static org.mockito.Mockito.mock;
23+
import static org.mockito.Mockito.when;
24+
25+
import org.apache.logging.log4j.Level;
26+
import org.junit.jupiter.api.AfterEach;
27+
import org.junit.jupiter.api.BeforeEach;
28+
import org.junit.jupiter.api.Test;
29+
import org.junit.jupiter.api.extension.ExtendWith;
30+
import org.junit.jupiter.api.parallel.ResourceLock;
31+
import uk.org.webcompere.systemstubs.SystemStubs;
32+
import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension;
33+
34+
@ExtendWith(SystemStubsExtension.class)
35+
@ResourceLock("log4j2.StatusLogger")
36+
class StatusLoggerFailingListenerTest {
37+
38+
public static final StatusLogger STATUS_LOGGER = StatusLogger.getLogger();
39+
40+
private StatusListener listener;
41+
42+
@BeforeEach
43+
void createAndRegisterListener() {
44+
listener = mock(StatusListener.class);
45+
STATUS_LOGGER.registerListener(listener);
46+
}
47+
48+
@AfterEach
49+
void unregisterListener() {
50+
STATUS_LOGGER.removeListener(listener);
51+
}
52+
53+
@Test
54+
void logging_with_failing_listener_should_not_cause_stack_overflow() throws Exception {
55+
56+
// Set up a failing listener on `log(StatusData)`
57+
when(listener.getStatusLevel()).thenReturn(Level.ALL);
58+
final Exception listenerFailure = new RuntimeException("test failure " + Math.random());
59+
doThrow(listenerFailure).when(listener).log(any());
60+
61+
// Log something and verify exception dump
62+
final String stderr = SystemStubs.tapSystemErr(() -> STATUS_LOGGER.error("foo"));
63+
final String listenerFailureClassName = listenerFailure.getClass().getCanonicalName();
64+
assertThat(stderr).contains(listenerFailureClassName + ": " + listenerFailure.getMessage());
65+
}
66+
}

log4j-api-test/src/test/java/org/apache/logging/log4j/status/StatusLoggerLevelTest.java

+24-2
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,24 @@
1616
*/
1717
package org.apache.logging.log4j.status;
1818

19+
import static org.apache.logging.log4j.status.StatusLogger.DEFAULT_FALLBACK_LISTENER_LEVEL;
1920
import static org.assertj.core.api.Assertions.assertThat;
2021
import static org.mockito.Mockito.mock;
2122
import static org.mockito.Mockito.when;
2223

24+
import java.util.Properties;
2325
import org.apache.logging.log4j.Level;
2426
import org.junit.jupiter.api.Test;
27+
import uk.org.webcompere.systemstubs.SystemStubs;
2528

2629
class StatusLoggerLevelTest {
2730

2831
@Test
2932
void effective_level_should_be_the_least_specific_one() {
3033

3134
// Verify the initial level
32-
final StatusLogger logger = StatusLogger.getLogger();
33-
final Level fallbackListenerLevel = Level.ERROR;
35+
final StatusLogger logger = new StatusLogger();
36+
final Level fallbackListenerLevel = DEFAULT_FALLBACK_LISTENER_LEVEL;
3437
assertThat(logger.getLevel()).isEqualTo(fallbackListenerLevel);
3538

3639
// Register a less specific listener
@@ -82,4 +85,23 @@ void effective_level_should_be_the_least_specific_one() {
8285
logger.removeListener(listener1);
8386
assertThat(logger.getLevel()).isEqualTo(fallbackListenerLevel); // Verify that the level is changed
8487
}
88+
89+
@Test
90+
void invalid_level_should_cause_fallback_to_defaults() throws Exception {
91+
92+
// Create a `StatusLogger` configuration using an invalid level
93+
final Properties statusLoggerConfigProperties = new Properties();
94+
final String invalidLevelName = "FOO";
95+
statusLoggerConfigProperties.put(StatusLogger.DEFAULT_STATUS_LISTENER_LEVEL, invalidLevelName);
96+
final StatusLogger.Config[] statusLoggerConfigRef = {null};
97+
final String stderr = SystemStubs.tapSystemErr(
98+
() -> statusLoggerConfigRef[0] = new StatusLogger.Config(statusLoggerConfigProperties));
99+
final StatusLogger.Config statusLoggerConfig = statusLoggerConfigRef[0];
100+
101+
// Verify the stderr dump
102+
assertThat(stderr).contains("Failed reading the level");
103+
104+
// Verify the level
105+
assertThat(statusLoggerConfig.fallbackListenerLevel).isEqualTo(DEFAULT_FALLBACK_LISTENER_LEVEL);
106+
}
85107
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.logging.log4j.status;
18+
19+
import static org.apache.logging.log4j.status.StatusLogger.PropertiesUtilsDouble.readAllAvailableProperties;
20+
import static org.apache.logging.log4j.status.StatusLogger.PropertiesUtilsDouble.readProperty;
21+
import static org.assertj.core.api.Assertions.assertThat;
22+
import static uk.org.webcompere.systemstubs.SystemStubs.restoreSystemProperties;
23+
import static uk.org.webcompere.systemstubs.SystemStubs.withEnvironmentVariable;
24+
25+
import java.util.Arrays;
26+
import java.util.Map;
27+
import java.util.stream.Stream;
28+
import org.junit.jupiter.api.parallel.ResourceAccessMode;
29+
import org.junit.jupiter.api.parallel.ResourceLock;
30+
import org.junit.jupiter.api.parallel.Resources;
31+
import org.junit.jupiter.params.ParameterizedTest;
32+
import org.junit.jupiter.params.provider.MethodSource;
33+
34+
class StatusLoggerPropertiesUtilDoubleTest {
35+
36+
private static final String[] MATCHING_PROPERTY_NAMES = new String[] {
37+
// System properties for version range `[, 2.10)`
38+
"log4j2.StatusLogger.DateFormat",
39+
// System properties for version range `[2.10, 3)`
40+
"log4j2.statusLoggerDateFormat",
41+
// System properties for version range `[3,)`
42+
"log4j2.StatusLogger.dateFormat",
43+
// Environment variables
44+
"LOG4J_STATUS_LOGGER_DATE_FORMAT"
45+
};
46+
47+
private static final String[] NOT_MATCHING_PROPERTY_NAMES =
48+
new String[] {"log4j2.StatusLogger$DateFormat", "log4j2.StàtusLögger.DateFormat"};
49+
50+
private static final class TestCase {
51+
52+
private final boolean matching;
53+
54+
private final String propertyName;
55+
56+
private final String userProvidedPropertyName;
57+
58+
private TestCase(final boolean matching, final String propertyName, final String userProvidedPropertyName) {
59+
this.matching = matching;
60+
this.propertyName = propertyName;
61+
this.userProvidedPropertyName = userProvidedPropertyName;
62+
}
63+
64+
@Override
65+
public String toString() {
66+
return String.format("`%s` %s `%s`", propertyName, matching ? "==" : "!=", userProvidedPropertyName);
67+
}
68+
}
69+
70+
static Stream<TestCase> testCases() {
71+
return Stream.concat(
72+
testCases(true, MATCHING_PROPERTY_NAMES, MATCHING_PROPERTY_NAMES),
73+
testCases(false, MATCHING_PROPERTY_NAMES, NOT_MATCHING_PROPERTY_NAMES));
74+
}
75+
76+
private static Stream<TestCase> testCases(
77+
final boolean matching, final String[] propertyNames, final String[] userProvidedPropertyNames) {
78+
return Arrays.stream(propertyNames).flatMap(propertyName -> Arrays.stream(userProvidedPropertyNames)
79+
.map(userProvidedPropertyName -> new TestCase(matching, propertyName, userProvidedPropertyName)));
80+
}
81+
82+
@ParameterizedTest
83+
@MethodSource("testCases")
84+
@ResourceLock(value = Resources.SYSTEM_PROPERTIES, mode = ResourceAccessMode.READ_WRITE)
85+
void system_properties_should_work(final TestCase testCase) throws Exception {
86+
restoreSystemProperties(() -> {
87+
final String expectedValue = "foo";
88+
System.setProperty(testCase.propertyName, expectedValue);
89+
verifyProperty(testCase, expectedValue);
90+
});
91+
}
92+
93+
@ParameterizedTest
94+
@MethodSource("testCases")
95+
@ResourceLock(value = Resources.GLOBAL, mode = ResourceAccessMode.READ_WRITE)
96+
void environment_variables_should_work(final TestCase testCase) throws Exception {
97+
final String expectedValue = "bar";
98+
withEnvironmentVariable(testCase.propertyName, expectedValue).execute(() -> {
99+
verifyProperty(testCase, expectedValue);
100+
});
101+
}
102+
103+
private static void verifyProperty(final TestCase testCase, final String expectedValue) {
104+
final Map<String, Object> normalizedProperties = readAllAvailableProperties();
105+
final String actualValue = readProperty(normalizedProperties, testCase.userProvidedPropertyName);
106+
if (testCase.matching) {
107+
assertThat(actualValue).describedAs("" + testCase).isEqualTo(expectedValue);
108+
} else {
109+
assertThat(actualValue).describedAs("" + testCase).isNull();
110+
}
111+
}
112+
}

0 commit comments

Comments
 (0)