/*
* Copyright 2016-present Facebook, Inc.
*
* Licensed 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 com.facebook.buck.event.listener;
import static com.facebook.buck.log.MachineReadableLogConfig.PREFIX_EXIT_CODE;
import static com.facebook.buck.log.MachineReadableLogConfig.PREFIX_INVOCATION_INFO;
import static com.facebook.buck.log.MachineReadableLogConfig.PREFIX_PERFTIMES_COMPLETE;
import static com.facebook.buck.log.MachineReadableLogConfig.PREFIX_PERFTIMES_UPDATE;
import static com.facebook.buck.log.MachineReadableLogConfig.PREFIX_UPLOAD_TO_CACHE_STATS;
import com.facebook.buck.artifact_cache.ArtifactCacheEvent;
import com.facebook.buck.artifact_cache.HttpArtifactCacheEvent;
import com.facebook.buck.event.BuckEventListener;
import com.facebook.buck.event.CommandEvent;
import com.facebook.buck.event.ParsingEvent;
import com.facebook.buck.event.WatchmanStatusEvent;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.log.CacheUploadInfo;
import com.facebook.buck.log.InvocationInfo;
import com.facebook.buck.log.Logger;
import com.facebook.buck.log.views.JsonViews;
import com.facebook.buck.model.BuildId;
import com.facebook.buck.parser.ParseEvent;
import com.facebook.buck.rules.BuildRuleEvent;
import com.facebook.buck.util.BuckConstant;
import com.facebook.buck.util.ObjectMappers;
import com.facebook.buck.util.autosparse.AutoSparseStateEvents;
import com.facebook.buck.util.versioncontrol.VersionControlStatsEvent;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.google.common.base.Charsets;
import com.google.common.eventbus.Subscribe;
import java.io.BufferedOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.OptionalInt;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class MachineReadableLoggerListener implements BuckEventListener {
private static final Logger LOG = Logger.get(MachineReadableLoggerListener.class);
private static final byte[] NEWLINE = "\n".getBytes(Charsets.UTF_8);
private static final int SHUTDOWN_TIMEOUT_SECONDS = 30;
private final InvocationInfo info;
private final ExecutorService executor;
private final ProjectFilesystem filesystem;
private final ObjectWriter objectWriter;
private BufferedOutputStream outputStream;
// Values to be written in the end of the log.
private OptionalInt exitCode = OptionalInt.empty();
// Cache upload statistics
private AtomicInteger cacheUploadSuccessCount = new AtomicInteger();
private AtomicInteger cacheUploadFailureCount = new AtomicInteger();
public MachineReadableLoggerListener(
InvocationInfo info, ProjectFilesystem filesystem, ExecutorService executor)
throws FileNotFoundException {
this.info = info;
this.filesystem = filesystem;
this.executor = executor;
this.objectWriter =
ObjectMappers.legacyCreate()
.copy()
.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false)
.writerWithView(JsonViews.MachineReadableLog.class);
this.outputStream =
new BufferedOutputStream(
new FileOutputStream(getLogFilePath().toFile(), /* append */ true));
writeToLog(PREFIX_INVOCATION_INFO, info);
}
@Subscribe
public void versionControlStats(VersionControlStatsEvent versionControlStatsEvent) {
writeToLog("SourceControlInformation", versionControlStatsEvent);
}
@Subscribe
public void parseStarted(ParseEvent.Started event) {
writeToLog("ParseStarted", event);
}
@Subscribe
public void parseFinished(ParseEvent.Finished event) {
writeToLog("ParseFinished", event);
}
@Subscribe
public void buildRuleEventFinished(BuildRuleEvent.Finished event) {
writeToLog("BuildRuleEvent.Finished", event);
}
@Subscribe
public void commandFinished(CommandEvent.Finished event) {
exitCode = OptionalInt.of(event.getExitCode());
}
@Subscribe
public synchronized void commandInterrupted(CommandEvent.Interrupted event) {
exitCode = OptionalInt.of(event.getExitCode());
}
@Subscribe
public synchronized void watchmanFileCreation(WatchmanStatusEvent.FileCreation event) {
writeToLog("FileCreate", event);
}
@Subscribe
public synchronized void watchmanFileDeletion(WatchmanStatusEvent.FileDeletion event) {
writeToLog("FileDelete", event);
}
@Subscribe
public void watchmanOverflow(WatchmanStatusEvent.Overflow event) {
writeToLog("WatchmanOverflow", event);
}
@Subscribe
public synchronized void symlinkInvalidation(ParsingEvent.SymlinkInvalidation event) {
writeToLog("SymlinkInvalidation", event);
}
@Subscribe
public synchronized void environmentalChange(ParsingEvent.EnvVariableChange event) {
writeToLog("EnvChange", event);
}
@Subscribe
public synchronized void timePerfStatsEvent(PerfTimesEventListener.PerfTimesEvent event) {
writeToLog(
event instanceof PerfTimesEventListener.PerfTimesEvent.Complete
? PREFIX_PERFTIMES_COMPLETE
: PREFIX_PERFTIMES_UPDATE,
event);
}
@Subscribe
public synchronized void autosparseSparseRefreshStarted(
AutoSparseStateEvents.SparseRefreshStarted event) {
writeToLog("Autosparse.SparseRefreshStarted", event);
}
@Subscribe
public synchronized void autosparseSparseRefreshFinished(
AutoSparseStateEvents.SparseRefreshFinished event) {
writeToLog("Autosparse.SparseRefreshFinished", event);
}
@Subscribe
public synchronized void autosparseSparseRefreshFailed(
AutoSparseStateEvents.SparseRefreshFailed event) {
writeToLog("Autosparse.SparseRefreshFailed", event);
}
@Subscribe
public void onArtifactCacheEvent(HttpArtifactCacheEvent.Finished event) {
if (event.getOperation() == ArtifactCacheEvent.Operation.STORE) {
if (event.getStoreData().wasStoreSuccessful().orElse(false)) {
cacheUploadSuccessCount.incrementAndGet();
} else {
cacheUploadFailureCount.incrementAndGet();
}
}
}
private Path getLogFilePath() {
return filesystem
.resolve(info.getLogDirectoryPath())
.resolve(BuckConstant.BUCK_MACHINE_LOG_FILE_NAME);
}
private void writeToLog(final String prefix, final Object obj) {
executor.submit(() -> writeToLogImpl(prefix, obj));
}
private void writeToLogImpl(String prefix, Object obj) {
try {
byte[] serializedObj = objectWriter.writeValueAsBytes(obj);
outputStream.write((prefix + " ").getBytes(Charsets.UTF_8));
outputStream.write(serializedObj);
outputStream.write(NEWLINE);
outputStream.flush();
} catch (JsonProcessingException e) {
LOG.warn("Failed to process json for event type: %s ", prefix);
} catch (IOException e) {
LOG.debug("Failed to write to %s", BuckConstant.BUCK_MACHINE_LOG_FILE_NAME, e);
}
}
@Override
public void outputTrace(BuildId buildId) throws InterruptedException {
// IMPORTANT: logging the ExitCode must happen on the executor, otherwise random
// log lines will be overwritten as outputStream access is not thread safe.
@SuppressWarnings("unused")
Future<?> unused =
executor.submit(
() -> {
try {
writeToLogImpl(
PREFIX_UPLOAD_TO_CACHE_STATS,
CacheUploadInfo.of(cacheUploadSuccessCount, cacheUploadFailureCount));
outputStream.write(
String.format(PREFIX_EXIT_CODE + " {\"exitCode\":%d}", exitCode.orElse(-1))
.getBytes(Charsets.UTF_8));
outputStream.close();
} catch (IOException e) {
LOG.warn("Failed to close output stream.");
}
});
executor.shutdown();
// Allow SHUTDOWN_TIMEOUT_SECONDS seconds for already scheduled writeToLog calls
// to complete.
if (!executor.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
String error =
"Machine readable log failed to complete all jobs within timeout during shutdown";
LOG.error(error);
}
}
}