001/*
002 * Copyright © 2025 CUI-OpenSource-Software (info@cuioss.de)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package de.cuioss.benchmarking.common.util;
017
018import org.apache.commons.io.output.TeeOutputStream;
019
020import java.io.FileOutputStream;
021import java.io.IOException;
022import java.io.PrintStream;
023import java.nio.file.Files;
024import java.nio.file.Path;
025import java.time.LocalDateTime;
026import java.time.format.DateTimeFormatter;
027import java.util.logging.*;
028
029/**
030 * Centralized logging configuration for JMH benchmarks.
031 * Sets up logging to write to both console and a timestamped file in benchmark-results.
032 *
033 * <p>This class provides:
034 * <ul>
035 *   <li>Dual output to console and file via TeeOutputStream</li>
036 *   <li>Capture of all System.out and System.err output</li>
037 *   <li>Timestamped log files in benchmark results directory</li>
038 *   <li>Configurable java.util.logging levels</li>
039 * </ul>
040 *
041 */
042@SuppressWarnings("java:S106") // System.out and System.err usage is intentional for logging setup
043public final class BenchmarkLoggingSetup {
044
045    private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
046    private static final String ROOT_LOGGER_NAME = "";
047    private static final String DE_PACKAGE = "de";
048    private static final String DE_CUIOSS_PACKAGE = "de.cuioss";
049    private static final String DE_CUIOSS_BENCHMARKING_PACKAGE = "de.cuioss.benchmarking";
050    private static final String JMH_PACKAGE = "org.openjdk.jmh";
051
052    // Keep references to original streams
053    private static final PrintStream ORIGINAL_OUT = System.out;
054    private static final PrintStream ORIGINAL_ERR = System.err;
055
056    // Keep reference to file output stream for proper cleanup
057    private static FileOutputStream currentFileOut = null;
058
059    private BenchmarkLoggingSetup() {
060        // Utility class
061    }
062
063    /**
064     * Configures logging to write to both console and a timestamped file in benchmark-results.
065     * Also redirects System.out and System.err to capture all console output.
066     *
067     * @param benchmarkResultsDir the directory where benchmark results are stored
068     */
069    // cui-rewrite:disable CuiLoggerStandardsRecipe
070    // System.err is appropriate here as logging infrastructure is not yet set up
071    public static void configureLogging(String benchmarkResultsDir) {
072        // Prepare paths and directories
073        Path resultsPath = Path.of(benchmarkResultsDir);
074        try {
075            Files.createDirectories(resultsPath);
076        } catch (IOException e) {
077            System.err.println("Failed to create benchmark results directory: " + e.getMessage());
078            System.err.println("Continuing with console-only logging");
079            return;
080        }
081
082        String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMAT);
083        String logFileName = "benchmark-run_%s.log".formatted(timestamp);
084        Path logFile = resultsPath.resolve(logFileName);
085
086        try {
087            // Close any previous file output stream
088            closeCurrentFileOut();
089
090            // Create new file output stream - kept open for the duration of the benchmark
091            currentFileOut = new FileOutputStream(logFile.toFile(), true);
092
093            TeeOutputStream teeOut = new TeeOutputStream(ORIGINAL_OUT, currentFileOut);
094            PrintStream newOut = new PrintStream(teeOut, true); // auto-flush enabled
095
096            TeeOutputStream teeErr = new TeeOutputStream(ORIGINAL_ERR, currentFileOut);
097            PrintStream newErr = new PrintStream(teeErr, true); // auto-flush enabled
098
099            // Redirect System.out and System.err
100            System.setOut(newOut);
101            System.setErr(newErr);
102
103            // Configure java.util.logging
104            configureJavaUtilLogging(resultsPath, timestamp);
105
106            // Log configuration success
107            System.out.println("Benchmark logging configured - writing to: " + logFile);
108            System.out.println("All console output (System.out/err and JMH) will be captured to both console and file");
109
110        } catch (IOException e) {
111            System.err.println("Failed to configure file logging: " + e.getMessage());
112            System.err.println("Continuing with console-only logging");
113            closeCurrentFileOut();
114        }
115    }
116
117    private static void closeCurrentFileOut() {
118        if (currentFileOut != null) {
119            try {
120                currentFileOut.close();
121            } catch (IOException e) {
122                // Ignore close errors
123            } finally {
124                currentFileOut = null;
125            }
126        }
127    }
128
129
130    private static void configureJavaUtilLogging(Path resultsPath, String timestamp) throws IOException {
131        Logger rootLogger = Logger.getLogger(ROOT_LOGGER_NAME);
132
133        Handler[] handlers = rootLogger.getHandlers();
134        for (Handler handler : handlers) {
135            rootLogger.removeHandler(handler);
136        }
137
138        ConsoleHandler consoleHandler = new ConsoleHandler();
139        consoleHandler.setLevel(Level.INFO);
140        consoleHandler.setFormatter(new SimpleFormatter());
141        rootLogger.addHandler(consoleHandler);
142
143        String logFileName = "benchmark-jul_%s.log".formatted(timestamp);
144        Path julLogFile = resultsPath.resolve(logFileName);
145        FileHandler fileHandler = new FileHandler(julLogFile.toString(), true);
146        fileHandler.setLevel(Level.ALL);
147        fileHandler.setFormatter(new SimpleFormatter());
148        rootLogger.addHandler(fileHandler);
149
150        rootLogger.setLevel(Level.INFO);
151
152        // Configure de.cuioss packages
153        configurePackageLogging(DE_PACKAGE, Level.INFO);
154        configurePackageLogging(DE_CUIOSS_PACKAGE, Level.INFO);
155        configurePackageLogging(DE_CUIOSS_BENCHMARKING_PACKAGE, Level.INFO);
156
157        // Disable verbose JMH internal logging
158        configurePackageLogging(JMH_PACKAGE, Level.WARNING);
159    }
160
161    private static void configurePackageLogging(String packageName, Level level) {
162        Logger.getLogger(packageName).setLevel(level);
163    }
164
165}