package com.hubspot.singularity.executor.task; import java.lang.ProcessBuilder.Redirect; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import org.slf4j.Logger; import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.hubspot.singularity.SingularityS3FormatHelper; import com.hubspot.singularity.SingularityTaskId; import com.hubspot.singularity.executor.SingularityExecutorLogrotateFrequency; import com.hubspot.singularity.executor.TemplateManager; import com.hubspot.singularity.executor.config.SingularityExecutorConfiguration; import com.hubspot.singularity.SingularityS3UploaderFile; import com.hubspot.singularity.executor.models.LogrotateCronTemplateContext; import com.hubspot.singularity.executor.models.LogrotateTemplateContext; import com.hubspot.singularity.runner.base.configuration.SingularityRunnerBaseConfiguration; import com.hubspot.singularity.runner.base.shared.JsonObjectFileHelper; import com.hubspot.singularity.runner.base.shared.S3UploadMetadata; import com.hubspot.singularity.runner.base.shared.SimpleProcessManager; import com.hubspot.singularity.runner.base.shared.TailMetadata; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; public class SingularityExecutorTaskLogManager { private final SingularityExecutorTaskDefinition taskDefinition; private final TemplateManager templateManager; private final SingularityRunnerBaseConfiguration baseConfiguration; private final SingularityExecutorConfiguration configuration; private final Logger log; private final JsonObjectFileHelper jsonObjectFileHelper; private final SingularityExecutorLogrotateFrequency logrotateFrequency; public SingularityExecutorTaskLogManager(SingularityExecutorTaskDefinition taskDefinition, TemplateManager templateManager, SingularityRunnerBaseConfiguration baseConfiguration, SingularityExecutorConfiguration configuration, Logger log, JsonObjectFileHelper jsonObjectFileHelper) { this.log = log; this.taskDefinition = taskDefinition; this.templateManager = templateManager; this.configuration = configuration; this.baseConfiguration = baseConfiguration; this.jsonObjectFileHelper = jsonObjectFileHelper; this.logrotateFrequency = taskDefinition.getExecutorData().getLogrotateFrequency().or(configuration.getLogrotateFrequency()); } public void setup() { ensureServiceOutExists(); writeLogrotateFile(); writeTailMetadata(false); writeS3MetadataFileForRotatedFiles(false); } @SuppressFBWarnings private boolean writeS3MetadataFileForRotatedFiles(boolean finished) { final Path serviceLogOutPath = taskDefinition.getServiceLogOutPath(); final Path serviceLogParent = serviceLogOutPath.getParent(); final Path logrotateDirectory = serviceLogParent.resolve(configuration.getLogrotateToDirectory()); List<String> handledLogs = new ArrayList<>(); int index = 1; boolean result = true; for (SingularityS3UploaderFile additionalFile : taskDefinition.getExecutorData().getS3UploaderAdditionalFiles()) { Path directory = additionalFile.getDirectory().isPresent() ? taskDefinition.getTaskDirectoryPath().resolve(additionalFile.getDirectory().get()) : taskDefinition.getTaskDirectoryPath(); String fileGlob = additionalFile.getFilename() != null && additionalFile.getFilename().contains("*") ? additionalFile.getFilename() : String.format("%s*.[gb]z*", additionalFile.getFilename()); result = result && writeS3MetadataFile(additionalFile.getS3UploaderFilenameHint().or(String.format("file%d", index)), directory, fileGlob, additionalFile.getS3UploaderBucket(), additionalFile.getS3UploaderKeyPattern(), finished, additionalFile.getS3StorageClass().or(taskDefinition.getExecutorData().getS3StorageClass()), additionalFile.getApplyS3StorageClassAfterBytes().or(taskDefinition.getExecutorData().getApplyS3StorageClassAfterBytes())); index++; handledLogs.add(additionalFile.getFilename()); } // Allow an additional file to override the upload settings for service.log if (!handledLogs.contains(taskDefinition.getServiceLogFileName())) { result = result && writeS3MetadataFile("default", logrotateDirectory, String.format("%s*.[gb]z*", taskDefinition.getServiceLogOutPath().getFileName()), Optional.<String>absent(), Optional.<String>absent(), finished, taskDefinition.getExecutorData().getS3StorageClass(), taskDefinition.getExecutorData().getApplyS3StorageClassAfterBytes()); } return result; } private void writeLogrotateFile() { log.info("Writing logrotate configuration file to {}", getLogrotateConfPath()); templateManager.writeLogrotateFile(getLogrotateConfPath(), new LogrotateTemplateContext(configuration, taskDefinition)); if (logrotateFrequency.getCronSchedule().isPresent()) { log.info("Writing logrotate cron entry with schedule '{}' to {}", logrotateFrequency.getCronSchedule().get(), getLogrotateCronPath()); templateManager.writeCronEntryForLogrotate(getLogrotateCronPath(), new LogrotateCronTemplateContext(configuration, taskDefinition, logrotateFrequency)); } } @SuppressFBWarnings public boolean teardown() { boolean writeTailMetadataSuccess = writeTailMetadata(true); ensureServiceOutExists(); if (taskDefinition.shouldLogrotateLogFile()) { copyLogTail(); } boolean writeS3MetadataForNonLogRotatedFileSuccess = true; if (!taskDefinition.shouldLogrotateLogFile()) { writeS3MetadataForNonLogRotatedFileSuccess = writeS3MetadataFile("unrotated", taskDefinition.getServiceLogOutPath().getParent(), taskDefinition.getServiceLogOutPath().getFileName().toString(), Optional.<String>absent(), Optional.<String>absent(), true, taskDefinition.getExecutorData().getS3StorageClass(), taskDefinition.getExecutorData().getApplyS3StorageClassAfterBytes()); } if (manualLogrotate()) { boolean removeLogRotateFileSuccess = removeLogrotateFile(); removeEmptyServiceOut(); boolean writeS3MetadataForLogrotatedFilesSuccess = writeS3MetadataFileForRotatedFiles(true); return writeTailMetadataSuccess && removeLogRotateFileSuccess && writeS3MetadataForLogrotatedFilesSuccess && writeS3MetadataForNonLogRotatedFileSuccess; } else { return false; } } private void copyLogTail() { if (configuration.getTailLogLinesToSave() <= 0) { return; } final Path tailOfLogPath = taskDefinition.getServiceFinishedTailLogPath(); if (Files.exists(tailOfLogPath)) { log.debug("{} already existed, skipping tail", tailOfLogPath); return; } final List<String> cmd = ImmutableList.of( "tail", "-n", Integer.toString(configuration.getTailLogLinesToSave()), taskDefinition.getServiceLogOut()); try { new SimpleProcessManager(log).runCommand(cmd, Redirect.to(tailOfLogPath.toFile())); } catch (Throwable t) { log.error("Failed saving tail of log {} to {}", taskDefinition.getServiceLogOut(), taskDefinition.getServiceFinishedTailLogPath(), t); } } public boolean removeLogrotateFile() { boolean deleted = false; try { deleted = Files.deleteIfExists(getLogrotateConfPath()); if (logrotateFrequency.getCronSchedule().isPresent()) { boolean cronDeleted = Files.deleteIfExists(getLogrotateCronPath()); deleted = deleted || cronDeleted; } } catch (Throwable t) { log.trace("Couldn't delete {}", getLogrotateConfPath(), t); return false; } log.trace("Deleted {} : {}", getLogrotateConfPath(), deleted); return true; } public boolean manualLogrotate() { if (!Files.exists(getLogrotateConfPath())) { log.info("{} did not exist, skipping manual logrotation", getLogrotateConfPath()); return true; } final List<String> command = ImmutableList.of( configuration.getLogrotateCommand(), "-f", "-s", taskDefinition.getLogrotateStateFilePath().toString(), getLogrotateConfPath().toString()); try { new SimpleProcessManager(log).runCommand(command); return true; } catch (Throwable t) { log.warn("Tried to manually logrotate using {}, but caught", getLogrotateConfPath(), t); return false; } } private void ensureServiceOutExists() { try { if (!Files.exists(taskDefinition.getServiceLogOutPath())) { Files.createFile(taskDefinition.getServiceLogOutPath()); } } catch (FileAlreadyExistsException faee) { log.debug("Executor out {} already existed", taskDefinition.getServiceLogOut()); } catch (Throwable t) { log.error("Failed creating executor out {}", taskDefinition.getServiceLogOut(), t); } } private void removeEmptyServiceOut() { try { if (Files.exists(taskDefinition.getServiceLogOutPath()) && Files.size(taskDefinition.getServiceLogOutPath()) == 0) { Files.deleteIfExists(taskDefinition.getServiceLogOutPath()); } } catch (Throwable t) { log.error("Failed checking/deleting executor out {}", taskDefinition.getServiceLogOut(), t); } } private boolean writeTailMetadata(boolean finished) { if (!taskDefinition.getExecutorData().getLoggingTag().isPresent()) { if (!finished) { log.warn("Not writing logging metadata because logging tag is absent"); } return true; } final TailMetadata tailMetadata = new TailMetadata(taskDefinition.getServiceLogOut(), taskDefinition.getExecutorData().getLoggingTag().get(), taskDefinition.getExecutorData().getLoggingExtraFields(), finished); final Path path = TailMetadata.getTailMetadataPath(Paths.get(baseConfiguration.getLogWatcherMetadataDirectory()), baseConfiguration.getLogWatcherMetadataSuffix(), tailMetadata); return jsonObjectFileHelper.writeObject(tailMetadata, path, log); } private String getS3KeyPattern(String s3KeyPattern) { final SingularityTaskId singularityTaskId = getSingularityTaskId(); return SingularityS3FormatHelper.getS3KeyFormat(s3KeyPattern, singularityTaskId, taskDefinition.getExecutorData().getLoggingTag(), taskDefinition.getExecutorData().getRequestGroup().or(SingularityS3FormatHelper.DEFAULT_GROUP_NAME)); } private SingularityTaskId getSingularityTaskId() { return SingularityTaskId.valueOf(taskDefinition.getTaskId()); } public Path getLogrotateConfPath() { return Paths.get(configuration.getLogrotateConfDirectory()).resolve(taskDefinition.getTaskId()); } public Path getLogrotateCronPath() { return Paths.get(configuration.getCronDirectory()).resolve(taskDefinition.getTaskId() + ".logrotate"); } private boolean writeS3MetadataFile(String filenameHint, Path pathToS3Directory, String globForS3Files, Optional<String> s3Bucket, Optional<String> s3KeyPattern, boolean finished, Optional<String> s3StorageClass, Optional<Long> applyS3StorageClassAfterBytes) { final String s3UploaderBucket = s3Bucket.or(taskDefinition.getExecutorData().getDefaultS3Bucket()); if (Strings.isNullOrEmpty(s3UploaderBucket)) { log.warn("No s3 bucket specified, not writing s3 metadata for file matcher {}", globForS3Files); return false; } S3UploadMetadata s3UploadMetadata = new S3UploadMetadata(pathToS3Directory.toString(), globForS3Files, s3UploaderBucket, getS3KeyPattern(s3KeyPattern.or(taskDefinition.getExecutorData().getS3UploaderKeyPattern())), finished, Optional.<String> absent(), Optional.<Integer> absent(), Optional.<String> absent(), Optional.<String> absent(), Optional.<Long> absent(), s3StorageClass, applyS3StorageClassAfterBytes, Optional.<Boolean>absent()); String s3UploadMetadataFileName = String.format("%s-%s%s", taskDefinition.getTaskId(), filenameHint, baseConfiguration.getS3UploaderMetadataSuffix()); Path s3UploadMetadataPath = Paths.get(baseConfiguration.getS3UploaderMetadataDirectory()).resolve(s3UploadMetadataFileName); return jsonObjectFileHelper.writeObject(s3UploadMetadata, s3UploadMetadataPath, log); } }