This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven.git


The following commit(s) were added to refs/heads/master by this push:
     new aeec37f8e7 [MNG-8478] Fix formatting of timestamp in logger (#2046)
aeec37f8e7 is described below

commit aeec37f8e7f968c5faebd99db87f9b5974b1cc53
Author: Guillaume Nodet <gno...@gmail.com>
AuthorDate: Wed Jan 15 07:28:29 2025 +0100

    [MNG-8478] Fix formatting of timestamp in logger (#2046)
---
 .../java/org/apache/maven/api/MonotonicClock.java  |  52 +++++++
 .../org/apache/maven/api/MonotonicClockTest.java   | 150 ++++++++++++++++++++
 impl/maven-logging/pom.xml                         |   5 +
 .../org/apache/maven/slf4j/MavenBaseLogger.java    |  29 ++--
 .../maven/slf4j/SimpleLoggerConfiguration.java     |   8 +-
 .../maven/slf4j/MavenBaseLoggerTimestampTest.java  | 152 +++++++++++++++++++++
 6 files changed, 375 insertions(+), 21 deletions(-)

diff --git 
a/api/maven-api-core/src/main/java/org/apache/maven/api/MonotonicClock.java 
b/api/maven-api-core/src/main/java/org/apache/maven/api/MonotonicClock.java
index 5fa3de655c..963618e59f 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/MonotonicClock.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/MonotonicClock.java
@@ -19,6 +19,7 @@
 package org.apache.maven.api;
 
 import java.time.Clock;
+import java.time.Duration;
 import java.time.Instant;
 import java.time.ZoneId;
 import java.time.ZoneOffset;
@@ -77,6 +78,27 @@ public class MonotonicClock extends Clock {
         return get().instant();
     }
 
+    /**
+     * Returns the initialization time of this monotonic clock.
+     * This is a convenience method equivalent to {@code get().start()}.
+     *
+     * @return the instant when this monotonic clock was initialized
+     * @see #startInstant()
+     */
+    public static Instant start() {
+        return get().startInstant();
+    }
+
+    /**
+     * Returns the elapsed time since clock initialization.
+     * This is a convenience method equivalent to {@code get().elapsedTime()}.
+     *
+     * @return the duration since clock initialization
+     */
+    public static Duration elapsed() {
+        return get().elapsedTime();
+    }
+
     /**
      * Returns a monotonically increasing instant.
      * <p>
@@ -93,6 +115,36 @@ public class MonotonicClock extends Clock {
         return startInstant.plusNanos(elapsedNanos);
     }
 
+    /**
+     * Returns the wall clock time captured when this monotonic clock was 
initialized.
+     * <p>
+     * This instant serves as the base time from which all subsequent {@link 
#instant()}
+     * calls are calculated by adding the elapsed monotonic duration. This 
ensures
+     * consistency between the monotonic measurements and wall clock time.
+     *
+     * @return the initial wall clock instant when this clock was created
+     * @see #instant()
+     */
+    public Instant startInstant() {
+        return startInstant;
+    }
+
+    /**
+     * Returns the duration elapsed since this clock was initialized.
+     * <p>
+     * The returned duration is calculated using {@link System#nanoTime()}
+     * to ensure monotonic behavior. This duration represents the exact time
+     * span between clock initialization and the current instant.
+     *
+     * @return the duration since clock initialization
+     * @see #startInstant()
+     * @see #instant()
+     */
+    public Duration elapsedTime() {
+        long elapsedNanos = System.nanoTime() - startNanos;
+        return Duration.ofNanos(elapsedNanos);
+    }
+
     /**
      * Returns the zone ID of this clock, which is always UTC.
      *
diff --git 
a/api/maven-api-core/src/test/java/org/apache/maven/api/MonotonicClockTest.java 
b/api/maven-api-core/src/test/java/org/apache/maven/api/MonotonicClockTest.java
new file mode 100644
index 0000000000..de561fb591
--- /dev/null
+++ 
b/api/maven-api-core/src/test/java/org/apache/maven/api/MonotonicClockTest.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *   http://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.apache.maven.api;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class MonotonicClockTest {
+
+    @Test
+    @DisplayName("MonotonicClock singleton instance should always return the 
same instance")
+    void testSingletonInstance() {
+        MonotonicClock clock1 = MonotonicClock.get();
+        MonotonicClock clock2 = MonotonicClock.get();
+
+        assertSame(clock1, clock2, "Multiple calls to get() should return the 
same instance");
+    }
+
+    @Test
+    @DisplayName("MonotonicClock should always use UTC timezone")
+    void testClockTimezone() {
+        MonotonicClock clock = MonotonicClock.get();
+
+        assertEquals(ZoneOffset.UTC, clock.getZone(), "Clock should use UTC 
timezone");
+
+        // Verify that attempting to change timezone returns the same instance
+        Clock newClock = clock.withZone(ZoneId.systemDefault());
+        assertSame(clock, newClock, "withZone() should return the same clock 
instance");
+    }
+
+    @Test
+    @DisplayName("MonotonicClock should maintain monotonic time progression")
+    void testMonotonicBehavior() throws InterruptedException {
+        Instant first = MonotonicClock.now();
+        Thread.sleep(10); // Small delay
+        Instant second = MonotonicClock.now();
+        Thread.sleep(10); // Small delay
+        Instant third = MonotonicClock.now();
+
+        assertTrue(first.isBefore(second), "Time should progress forward 
between measurements");
+        assertTrue(second.isBefore(third), "Time should progress forward 
between measurements");
+    }
+
+    @Test
+    @DisplayName("MonotonicClock elapsed time should increase")
+    void testElapsedTime() throws InterruptedException {
+        Duration initial = MonotonicClock.elapsed();
+        Thread.sleep(50); // Longer delay for more reliable measurement
+        Duration later = MonotonicClock.elapsed();
+
+        assertTrue(later.compareTo(initial) > 0, "Elapsed time should 
increase");
+        assertTrue(
+                later.minus(initial).toMillis() >= 45,
+                "Elapsed time difference should be at least 45ms (accounting 
for some timing variance)");
+    }
+
+    @Test
+    @DisplayName("MonotonicClock start time should remain constant")
+    void testStartTime() throws InterruptedException {
+        Instant start1 = MonotonicClock.start();
+        Thread.sleep(10);
+        Instant start2 = MonotonicClock.start();
+
+        assertEquals(start1, start2, "Start time should remain constant");
+        assertNotNull(start1, "Start time should not be null");
+    }
+
+    @Nested
+    @DisplayName("Time consistency tests")
+    class TimeConsistencyTests {
+
+        @Test
+        @DisplayName("Current time should be after start time")
+        void testCurrentTimeAfterStart() {
+            Instant now = MonotonicClock.now();
+            Instant start = MonotonicClock.start();
+
+            assertTrue(now.isAfter(start), "Current time should be after start 
time");
+        }
+
+        @Test
+        @DisplayName("Elapsed time should match time difference")
+        void testElapsedTimeConsistency() {
+            MonotonicClock clock = MonotonicClock.get();
+            Instant now = clock.instant();
+            Duration elapsed = clock.elapsedTime();
+            Duration calculated = Duration.between(clock.startInstant(), now);
+
+            // Allow for small timing differences (1ms) due to execution time 
between measurements
+            assertTrue(
+                    Math.abs(elapsed.toMillis() - calculated.toMillis()) <= 1,
+                    "Elapsed time should match calculated duration between 
start and now");
+        }
+    }
+
+    @Test
+    @DisplayName("MonotonicClock should handle rapid successive calls")
+    void testRapidCalls() {
+        Instant[] instants = new Instant[1000];
+        for (int i = 0; i < instants.length; i++) {
+            instants[i] = MonotonicClock.now();
+        }
+
+        // Verify monotonic behavior across all measurements
+        for (int i = 1; i < instants.length; i++) {
+            assertTrue(
+                    instants[i].compareTo(instants[i - 1]) >= 0,
+                    "Time should never go backwards even with rapid successive 
calls");
+        }
+    }
+
+    @Test
+    @DisplayName("MonotonicClock should maintain reasonable alignment with 
system time")
+    void testSystemTimeAlignment() {
+        Instant monotonic = MonotonicClock.now();
+        Instant system = Instant.now();
+
+        // The difference should be relatively small (allow for 1 second max)
+        Duration difference = Duration.between(monotonic, system).abs();
+        assertTrue(difference.getSeconds() <= 1, "Monotonic time should be 
reasonably aligned with system time");
+    }
+}
diff --git a/impl/maven-logging/pom.xml b/impl/maven-logging/pom.xml
index ed451909b2..f6fcc118c2 100644
--- a/impl/maven-logging/pom.xml
+++ b/impl/maven-logging/pom.xml
@@ -50,6 +50,11 @@ under the License.
       <artifactId>junit-jupiter-api</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-params</artifactId>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>org.hamcrest</groupId>
       <artifactId>hamcrest</artifactId>
diff --git 
a/impl/maven-logging/src/main/java/org/apache/maven/slf4j/MavenBaseLogger.java 
b/impl/maven-logging/src/main/java/org/apache/maven/slf4j/MavenBaseLogger.java
index 6115844046..cd8d1e19f0 100644
--- 
a/impl/maven-logging/src/main/java/org/apache/maven/slf4j/MavenBaseLogger.java
+++ 
b/impl/maven-logging/src/main/java/org/apache/maven/slf4j/MavenBaseLogger.java
@@ -19,9 +19,9 @@
 package org.apache.maven.slf4j;
 
 import java.io.PrintStream;
-import java.time.Clock;
-import java.time.Duration;
-import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -146,9 +146,6 @@ import org.slf4j.spi.LocationAwareLogger;
  */
 public class MavenBaseLogger extends LegacyAbstractLogger {
 
-    private static final Clock MONOTONIC_CLOCK = Clock.tick(Clock.systemUTC(), 
Duration.ofMillis(1));
-    private static final Instant START_TIME = MonotonicClock.now();
-
     protected static final int LOG_LEVEL_TRACE = LocationAwareLogger.TRACE_INT;
     protected static final int LOG_LEVEL_DEBUG = LocationAwareLogger.DEBUG_INT;
     protected static final int LOG_LEVEL_INFO = LocationAwareLogger.INFO_INT;
@@ -165,7 +162,7 @@ public class MavenBaseLogger extends LegacyAbstractLogger {
 
     static final SimpleLoggerConfiguration CONFIG_PARAMS = new 
SimpleLoggerConfiguration();
 
-    private static boolean initialized = false;
+    static boolean initialized = false;
 
     static void lazyInit() {
         if (initialized) {
@@ -266,15 +263,6 @@ public class MavenBaseLogger extends LegacyAbstractLogger {
         }
     }
 
-    protected String getFormattedDate() {
-        Instant now = MonotonicClock.now();
-        String dateText;
-        synchronized (CONFIG_PARAMS.dateFormatter) {
-            dateText = CONFIG_PARAMS.dateFormatter.format(now);
-        }
-        return dateText;
-    }
-
     protected String computeShortName() {
         return name.substring(name.lastIndexOf(".") + 1);
     }
@@ -380,11 +368,14 @@ public class MavenBaseLogger extends LegacyAbstractLogger 
{
 
         // Append date-time if so configured
         if (CONFIG_PARAMS.showDateTime) {
-            if (CONFIG_PARAMS.dateFormatter != null) {
-                buf.append(getFormattedDate());
+            DateTimeFormatter formatter = CONFIG_PARAMS.dateFormatter;
+            if (formatter != null) {
+                ZonedDateTime zonedDateTime = 
MonotonicClock.now().atZone(ZoneId.systemDefault());
+                String dateText = formatter.format(zonedDateTime);
+                buf.append(dateText);
                 buf.append(SP);
             } else {
-                buf.append(Duration.between(START_TIME, 
MonotonicClock.now()).toMillis());
+                buf.append(MonotonicClock.elapsed().toMillis());
                 buf.append(SP);
             }
         }
diff --git 
a/impl/maven-logging/src/main/java/org/apache/maven/slf4j/SimpleLoggerConfiguration.java
 
b/impl/maven-logging/src/main/java/org/apache/maven/slf4j/SimpleLoggerConfiguration.java
index 63b3667402..0ea1a33498 100644
--- 
a/impl/maven-logging/src/main/java/org/apache/maven/slf4j/SimpleLoggerConfiguration.java
+++ 
b/impl/maven-logging/src/main/java/org/apache/maven/slf4j/SimpleLoggerConfiguration.java
@@ -53,8 +53,6 @@ public class SimpleLoggerConfiguration {
     boolean showDateTime = SHOW_DATE_TIME_DEFAULT;
 
     private static final String DATE_TIME_FORMAT_STR_DEFAULT = null;
-    private static String dateTimeFormatStr = DATE_TIME_FORMAT_STR_DEFAULT;
-
     DateTimeFormatter dateFormatter = null;
 
     private static final boolean SHOW_THREAD_NAME_DEFAULT = true;
@@ -90,6 +88,9 @@ public class SimpleLoggerConfiguration {
     private final Properties properties = new Properties();
 
     void init() {
+        // Reset state before initialization
+        dateFormatter = null;
+
         loadProperties();
 
         String defaultLogLevelString = 
getStringProperty(MavenBaseLogger.DEFAULT_LOG_LEVEL_KEY, null);
@@ -97,6 +98,9 @@ public class SimpleLoggerConfiguration {
             defaultLogLevel = stringToLevel(defaultLogLevelString);
         }
 
+        // local variable,
+        String dateTimeFormatStr;
+
         showLogName =
                 getBooleanProperty(MavenBaseLogger.SHOW_LOG_NAME_KEY, 
SimpleLoggerConfiguration.SHOW_LOG_NAME_DEFAULT);
         showShortLogName = 
getBooleanProperty(MavenBaseLogger.SHOW_SHORT_LOG_NAME_KEY, 
SHOW_SHORT_LOG_NAME_DEFAULT);
diff --git 
a/impl/maven-logging/src/test/java/org/apache/maven/slf4j/MavenBaseLoggerTimestampTest.java
 
b/impl/maven-logging/src/test/java/org/apache/maven/slf4j/MavenBaseLoggerTimestampTest.java
new file mode 100644
index 0000000000..b79eb19984
--- /dev/null
+++ 
b/impl/maven-logging/src/test/java/org/apache/maven/slf4j/MavenBaseLoggerTimestampTest.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *   http://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.apache.maven.slf4j;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.matchesPattern;
+
+class MavenBaseLoggerTimestampTest {
+    private ByteArrayOutputStream logOutput;
+    private PrintStream originalErr;
+    private static final String LOGGER_NAME = "test.logger";
+    private MavenBaseLogger logger;
+
+    @BeforeEach
+    void setUp() {
+        // Reset configuration before each test
+        System.clearProperty(MavenBaseLogger.SHOW_DATE_TIME_KEY);
+        System.clearProperty(MavenBaseLogger.DATE_TIME_FORMAT_KEY);
+
+        // Reset static initialization flag
+        MavenBaseLogger.initialized = false;
+
+        // Capture System.err
+        logOutput = new ByteArrayOutputStream();
+        originalErr = System.err;
+        System.setErr(new PrintStream(logOutput));
+    }
+
+    @AfterEach
+    void tearDown() {
+        System.setErr(originalErr);
+        System.clearProperty(MavenBaseLogger.SHOW_DATE_TIME_KEY);
+        System.clearProperty(MavenBaseLogger.DATE_TIME_FORMAT_KEY);
+        MavenBaseLogger.initialized = false;
+    }
+
+    @Test
+    void whenShowDateTimeIsFalse_shouldNotIncludeTimestamp() {
+        // Given
+        System.setProperty(MavenBaseLogger.SHOW_DATE_TIME_KEY, "false");
+        initializeLogger();
+
+        // When
+        logger.info("Test message");
+        String output = getLastLine(logOutput.toString());
+
+        // Then
+        assertThat(
+                "Should not include timestamp", output, 
matchesPattern("^\\[main\\] INFO test.logger - Test message$"));
+    }
+
+    @Test
+    void whenShowDateTimeIsTrue_withoutFormat_shouldShowElapsedTime() { // 
Changed test name and expectation
+        // Given
+        System.setProperty(MavenBaseLogger.SHOW_DATE_TIME_KEY, "true");
+        initializeLogger();
+
+        // When
+        logger.info("Test message");
+        String output = getLastLine(logOutput.toString());
+
+        // Then
+        assertThat(
+                "Should show elapsed time when no format specified",
+                output,
+                matchesPattern("^\\d+ \\[main\\] INFO test.logger - Test 
message$"));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"yyyy-MM-dd HH:mm:ss", "dd/MM/yyyy HH:mm:ss.SSS", 
"HH:mm:ss"})
+    void whenCustomDateFormat_shouldFormatCorrectly(String dateFormat) {
+        // Given
+        System.setProperty(MavenBaseLogger.SHOW_DATE_TIME_KEY, "true");
+        System.setProperty(MavenBaseLogger.DATE_TIME_FORMAT_KEY, dateFormat);
+        initializeLogger();
+
+        // When
+        logger.info("Test message");
+        String output = getLastLine(logOutput.toString());
+
+        // Then
+        String patternStr = dateFormat
+                .replace("yyyy", "\\d{4}")
+                .replace("MM", "\\d{2}")
+                .replace("dd", "\\d{2}")
+                .replace("HH", "\\d{2}")
+                .replace("mm", "\\d{2}")
+                .replace("ss", "\\d{2}")
+                .replace("SSS", "\\d{3}")
+                .replace("/", "\\/")
+                .replace(".", "\\.");
+
+        assertThat(
+                "Should match custom date format",
+                output,
+                matchesPattern("^" + patternStr + " \\[main\\] INFO 
test.logger - Test message$"));
+    }
+
+    @Test
+    void whenInvalidDateFormat_shouldUseElapsedMillis() {
+        // Given
+        System.setProperty(MavenBaseLogger.SHOW_DATE_TIME_KEY, "true");
+        System.setProperty(MavenBaseLogger.DATE_TIME_FORMAT_KEY, 
"invalid-format");
+        initializeLogger();
+
+        // When
+        logger.info("Test message");
+        String output = getLastLine(logOutput.toString());
+
+        // Then
+        assertThat(
+                "Should show elapsed milliseconds when format is invalid",
+                output,
+                matchesPattern("^\\d+ \\[main\\] INFO test.logger - Test 
message$"));
+    }
+
+    private void initializeLogger() {
+        MavenBaseLogger.CONFIG_PARAMS.init();
+        logger = new MavenBaseLogger(LOGGER_NAME);
+        logOutput.reset();
+    }
+
+    private String getLastLine(String output) {
+        String[] lines = output.split("\\R");
+        return lines[lines.length - 1].trim();
+    }
+}

Reply via email to