/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.component.workflow.execution.internal; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.Writer; import java.util.List; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import de.rcenvironment.core.component.execution.api.ConsoleRow; import de.rcenvironment.core.component.workflow.execution.api.ConsoleRowLogService; import de.rcenvironment.core.configuration.ConfigurationService; import de.rcenvironment.core.configuration.ConfigurationService.ConfigurablePathId; import de.rcenvironment.core.toolkitbridge.transitional.ConcurrencyUtils; import de.rcenvironment.core.utils.common.StringUtils; import de.rcenvironment.toolkit.modules.concurrency.api.TaskDescription; /** * Default {@link ConsoleRowLogService} implementation. * * @author Robert Mischke */ public class ConsoleRowLogServiceImpl implements ConsoleRowLogService { /** * The number of characters that the log buffer may accumulate before a warning message is logged. Added to check whether background * buffering with a low-priority writer thread consumes too much memory in long-running, high-CPU-load workflows. */ private static final int BUFFERED_CHARACTER_COUNT_WARNING_THRESHOLD = 2 * 1024 * 1024; // arbitrary // only needs to be defined to enable; no specific value needed private static final String SYSTEM_PROPERTY_FOR_ACTIVATION = "rce.internal.writeCombinedConsoleLogs"; private LinkedBlockingQueue<ConsoleRow> outputQueue; private Writer fileWriter; private AtomicInteger bufferedCharacterCount = new AtomicInteger(); private volatile BackgroundLogWriterTask backgroundWriterTask; private volatile Future<?> backgroundTaskFuture; private ConfigurationService configurationService; private File logFile; private boolean enabled = false; private final Log log = LogFactory.getLog(getClass()); /** * A background task to write log output to a file. The executing thread's priority is set to the given value, and reset after logging * has finished. * * @author Robert Mischke */ // TODO rework to reusable stand-alone class? - misc_ro private final class BackgroundLogWriterTask implements Runnable { private final int threadPriority; private final Writer writer; private boolean anythingLogged; private BackgroundLogWriterTask(Writer writer, int threadPriority) { this.writer = writer; this.threadPriority = threadPriority; } @Override @TaskDescription("Background log writing") public void run() { final Thread currentThread = Thread.currentThread(); final int originalPriority = currentThread.getPriority(); currentThread.setPriority(threadPriority); try { runLogging(); } finally { // reset to previous value currentThread.setPriority(originalPriority); } }; private void runLogging() { final Thread currentThread = Thread.currentThread(); final ConsoleRowFormatter consoleRowFormatter = new ConsoleRowFormatter(); anythingLogged = false; try { while (!currentThread.isInterrupted()) { ConsoleRow row = outputQueue.take(); // subtract length of contained test string modifyCharacterCount(-row.getPayload().length()); try { // TODO add an explicit flush mechanism to ensure rows are on disk after a // given time? writer.append(consoleRowFormatter.toCombinedLogFileFormat(row)); anythingLogged = true; } catch (IOException e) { log.error(e); break; } } } catch (InterruptedException e) { log.debug("Background log writer interrupted"); } try { writer.close(); // TODO bad encapsulation; improve when reworking this class if (!anythingLogged) { logFile.delete(); } } catch (IOException e) { log.error(e); } } } /** * Constructor. * * @param filename the filename to log to * @param autoCloseOnShutdown if true, a shutdown hook will be registered that calls {@link #close()} * @throws IOException if creating the log file failed */ public ConsoleRowLogServiceImpl() throws IOException { outputQueue = new LinkedBlockingQueue<ConsoleRow>(); } /** * OSGi-DS lifecycle method. Starts background logging of accumulated log lines. */ public void activate() { enabled = (System.getProperty(SYSTEM_PROPERTY_FOR_ACTIVATION) != null); if (!enabled) { log.debug("Combined workflow console log is disabled"); return; } // TODO improve filename, locking/uniqueness etc. String logFileName = StringUtils.format("console.combined.%d.log", System.currentTimeMillis()); File outputDir = configurationService.getConfigurablePath(ConfigurablePathId.PROFILE_OUTPUT); logFile = new File(outputDir, logFileName); try { fileWriter = new BufferedWriter(new FileWriter(logFile)); // TODO use the thread pool instead? backgroundWriterTask = new BackgroundLogWriterTask(fileWriter, Thread.MIN_PRIORITY); backgroundTaskFuture = ConcurrencyUtils.getAsyncTaskService().submit(backgroundWriterTask, "Common ConsoleRow log " + logFile.getAbsolutePath()); log.debug("Logging combined workflow console output to " + logFileName + " (NOTE: may not capture all output yet)"); // TODO 5.0 } catch (IOException e) { log.error("Failed to set up background console logging to " + logFileName, e); } } /** * OSGi-DS lifecycle method. Stops logging and closes the output file. * * Note: Closing the log file may happen asynchronously. */ public void deactivate() { if (!enabled) { return; } if (backgroundTaskFuture != null) { backgroundTaskFuture.cancel(true); } } /** * Enqueues a {@link ConsoleRow} to log. This method is thread-safe. * * @param row the {@link ConsoleRow} to log */ public void append(ConsoleRow row) { // add the length of contained payload; note that this is not an exact measure of log output to write modifyCharacterCount(row.getPayload().length()); outputQueue.add(row); } /** * Modifies the counter that keeps track of how many characters are stored in the background buffer. Also checks against the defined * size limit. * * @param delta the "delta" to add to the counter; may be negative to decrement the counter */ private void modifyCharacterCount(int delta) { int newTotal = bufferedCharacterCount.addAndGet(delta); if (delta > 0) { if (newTotal >= BUFFERED_CHARACTER_COUNT_WARNING_THRESHOLD) { log.warn(StringUtils.format("Background log buffer has grown to %d characters", newTotal)); } } else { // consistency check if (newTotal < 0) { log.error("Integrity violation: buffer count decremented below zero"); } } } /** * Enqueues {@link ConsoleRow} entries to log. This method is thread-safe, although there is no guarantee that lists of * {@link ConsoleRow}s passed by concurrent calls are appended as uninterrupted sequences. * * @param rows the {@link ConsoleRow}s to log */ @Override public void processConsoleRows(List<ConsoleRow> rows) { if (!enabled) { return; } // add total string length to counter int charCount = 0; for (ConsoleRow row : rows) { charCount += row.getPayload().length(); } modifyCharacterCount(charCount); // add to buffer outputQueue.addAll(rows); } /** * OSGi-DS injection method. * * @param newInstance the new service instance */ public void bindConfigurationService(ConfigurationService newInstance) { this.configurationService = newInstance; } }