11/*
2- * Copyright (c) 2022, 2024 Eclipse Foundation and/or its affiliates. All rights reserved.
2+ * Copyright (c) 2022, 2025 Contributors to the Eclipse Foundation
33 *
44 * This program and the accompanying materials are made available under the
55 * terms of the Eclipse Public License v. 2.0, which is available at
2020import java .io .File ;
2121import java .io .FileOutputStream ;
2222import java .io .IOException ;
23- import java .lang .System .Logger ;
2423import java .nio .charset .Charset ;
2524import java .nio .file .Files ;
2625import java .nio .file .StandardCopyOption ;
2726import java .time .LocalDateTime ;
2827import java .time .format .DateTimeFormatter ;
28+ import java .util .concurrent .ConcurrentLinkedQueue ;
29+ import java .util .concurrent .atomic .AtomicBoolean ;
2930import java .util .concurrent .locks .ReentrantLock ;
30- import java .util .function .Supplier ;
31+ import java .util .logging .Level ;
32+ import java .util .logging .Logger ;
3133
34+ import org .glassfish .main .jul .record .GlassFishLogRecord ;
3235import org .glassfish .main .jul .tracing .GlassFishLoggingTracer ;
3336
34- import static java .lang . System . Logger . Level .ERROR ;
35- import static java .lang . System . Logger . Level .INFO ;
37+ import static java .util . logging . Level .INFO ;
38+ import static java .util . logging . Level .SEVERE ;
3639import static org .glassfish .main .jul .tracing .GlassFishLoggingTracer .trace ;
3740
3841
4952 * @author David Matejcek
5053 */
5154public class LogFileManager {
52- private static final Logger LOG = System .getLogger (LogFileManager .class .getName ());
5355
5456 private static final DateTimeFormatter SUFFIX_FORMATTER = DateTimeFormatter .ofPattern ("yyyy-MM-dd'T'HH-mm-ss" );
5557
@@ -181,10 +183,9 @@ public void rollIfFileNotEmpty() {
181183 */
182184 public void roll () {
183185 lock .lock ();
184- try {
186+ try ( AsyncLogger logger = new AsyncLogger ()) {
185187 final boolean wasOutputEnabled = isOutputEnabled ();
186- logInfoAsync (
187- () -> "Rolling the file " + this .logFile + "; output was originally enabled: " + wasOutputEnabled );
188+ logger .logInfo ("Rolling the file " + this .logFile + "; output was originally enabled: " + wasOutputEnabled );
188189 disableOutput ();
189190 File archivedFile = null ;
190191 try {
@@ -193,11 +194,11 @@ public void roll() {
193194 }
194195 archivedFile = prepareAchivedLogFileTarget ();
195196 trace (LogFileManager .class , "Archived file: " + archivedFile );
196- moveFile (logFile , archivedFile );
197+ moveFile (logFile , archivedFile , logger );
197198 forceOSFilesync (logFile );
198199 return ;
199200 } catch (Exception e ) {
200- logErrorAsync ("Error, could not rotate log file " + logFile , e );
201+ logger . logError ("Error, could not rotate log file " + logFile , e );
201202 } finally {
202203 if (wasOutputEnabled ) {
203204 enableOutput ();
@@ -322,36 +323,23 @@ private void forceOSFilesync(final File file) throws IOException {
322323 }
323324
324325
325- private void moveFile (final File logFileToArchive , final File target ) throws IOException {
326- logInfoAsync (() -> "Archiving file " + logFileToArchive + " to " + target );
326+ private void moveFile (final File logFileToArchive , final File target , final AsyncLogger logger ) throws IOException {
327+ logger . logInfo ( "Archiving file " + logFileToArchive + " to " + target );
327328 try {
328329 Files .move (logFileToArchive .toPath (), target .toPath (), StandardCopyOption .ATOMIC_MOVE );
329330 } catch (UnsupportedOperationException | IOException e ) {
330331 // If we don't succeed with file rename which most likely can happen on
331332 // Windows because of multiple file handles opened. We go through Plan B to
332333 // copy bytes explicitly to a renamed file.
333334 // Can happen on some windows file systems - then we try non-atomic version at least.
334- logErrorAsync (String .format (
335+ logger . logError (String .format (
335336 "File %s could not be renamed to %s atomically, now trying to move it without this request." ,
336337 logFileToArchive , target ), e );
337338 Files .move (logFileToArchive .toPath (), target .toPath ());
338339 }
339340 }
340341
341342
342- /**
343- * This logs in a separate thread to avoid deadlocks. The separate thread can be blocked when
344- * the LogRecordBuffer is full while the LogFileManager is still locked and doesn't process
345- * any records until it finishes rolling the file.
346- * <p>
347- * The count of messages is limited, so we can do this.
348- */
349- private void logInfoAsync (final Supplier <String > message ) {
350- trace (getClass (), message );
351- new Thread (() -> LOG .log (INFO , message ), "LogFileManager-Async-Info-Logger" ).start ();
352- }
353-
354-
355343 /**
356344 * This logs in a separate thread to avoid deadlocks. The separate thread can be blocked when
357345 * the LogRecordBuffer is full while the LogFileManager is still locked and doesn't process
@@ -362,8 +350,64 @@ private void logInfoAsync(final Supplier<String> message) {
362350 * However it is not suitable for all errors - if we cannot write to the file, this would just create
363351 * another record which could not be written.
364352 */
365- private void logErrorAsync (final String message , final Exception exception ) {
366- GlassFishLoggingTracer .error (getClass (), message , exception );
367- new Thread (() -> LOG .log (ERROR , message , exception ), "LogFileManager-Async-Error-Logger" ).start ();
353+ private static class AsyncLogger extends Thread implements AutoCloseable {
354+
355+ private final AtomicBoolean stop ;
356+ private final ConcurrentLinkedQueue <AsyncLogRecord > queue ;
357+ private final Logger logger ;
358+
359+ private AsyncLogger () {
360+ super ("LogFileManagerAsyncLogger" );
361+ setDaemon (true );
362+ this .queue = new ConcurrentLinkedQueue <>();
363+ this .stop = new AtomicBoolean ();
364+ this .logger = Logger .getLogger (LogFileManager .class .getName (), null );
365+ start ();
366+ }
367+
368+ void logInfo (final String message ) {
369+ trace (getClass (), message );
370+ queue .add (new AsyncLogRecord (INFO , message , null ));
371+ }
372+
373+ void logError (final String message , final Exception exception ) {
374+ GlassFishLoggingTracer .error (getClass (), message , exception );
375+ queue .add (new AsyncLogRecord (SEVERE , message , exception ));
376+ }
377+
378+ @ Override
379+ public void close () {
380+ this .stop .set (true );
381+ }
382+
383+ @ Override
384+ public void run () {
385+ while (!stop .get ()) {
386+ drainQueue ();
387+ Thread .onSpinWait ();
388+ }
389+ drainQueue ();
390+ }
391+
392+ private void drainQueue () {
393+ while (true ) {
394+ AsyncLogRecord record = queue .poll ();
395+ if (record == null ) {
396+ break ;
397+ }
398+ logger .log (record );
399+ }
400+ }
401+ }
402+
403+
404+ private static class AsyncLogRecord extends GlassFishLogRecord {
405+
406+ private static final long serialVersionUID = -8159574547676058852L ;
407+
408+ AsyncLogRecord (Level level , String message , Throwable error ) {
409+ super (level , message , true );
410+ setThrown (error );
411+ }
368412 }
369413}
0 commit comments