All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hubspot.singularity.smtp.MailTemplateHelpers Maven / Gradle / Ivy

package com.hubspot.singularity.smtp;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.apache.commons.lang3.text.WordUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Optional;
import com.google.common.collect.Lists;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.hubspot.singularity.helpers.MesosUtils;
import com.hubspot.mesos.json.MesosFileChunkObject;
import com.hubspot.singularity.ExtendedTaskState;
import com.hubspot.singularity.SingularityDisasterDataPoint;
import com.hubspot.singularity.SingularityEmailType;
import com.hubspot.singularity.SingularityMailDisasterDataPoint;
import com.hubspot.singularity.SingularityTask;
import com.hubspot.singularity.SingularityTaskHistoryUpdate;
import com.hubspot.singularity.SingularityTaskId;
import com.hubspot.singularity.SingularityTaskMetadata;
import com.hubspot.singularity.config.SMTPConfiguration;
import com.hubspot.singularity.config.SingularityConfiguration;
import com.hubspot.singularity.data.SandboxManager;

@Singleton
public class MailTemplateHelpers {

  private static final Logger LOG = LoggerFactory.getLogger(MailTemplateHelpers.class);

  private static final String TASK_LINK_FORMAT = "%s/task/%s";
  private static final String REQUEST_LINK_FORMAT = "%s/request/%s";
  private static final String LOG_LINK_FORMAT = "%s/task/%s/tail/%s";
  private static final String DEFAULT_TIMESTAMP_FORMAT = "MMM dd h:mm:ss a zzz";

  private final SandboxManager sandboxManager;

  private final Optional uiBaseUrl;
  private final Optional smtpConfiguration;
  private final Optional taskDatePattern;
  private final Optional timeZone;

  @Inject
  public MailTemplateHelpers(SandboxManager sandboxManager, SingularityConfiguration singularityConfiguration) {
    this.uiBaseUrl = singularityConfiguration.getSmtpConfigurationOptional().isPresent() ?
        singularityConfiguration.getSmtpConfiguration().getUiBaseUrl().or(singularityConfiguration.getUiConfiguration().getBaseUrl()) :
        singularityConfiguration.getUiConfiguration().getBaseUrl();
    this.sandboxManager = sandboxManager;
    this.smtpConfiguration = singularityConfiguration.getSmtpConfigurationOptional();
    if (this.smtpConfiguration.isPresent()) {
      this.taskDatePattern = Optional.of(this.smtpConfiguration.get().getMailerDatePattern());
      this.timeZone = Optional.of(this.smtpConfiguration.get().getMailerTimeZone());
    } else {
      this.taskDatePattern = Optional.absent();
      this.timeZone = Optional.absent();
    }
  }

  public String humanizeTimestamp(long timestamp) {
    if (taskDatePattern.isPresent() && timeZone.isPresent()) {
      return DateFormatUtils.format(timestamp, taskDatePattern.get(), timeZone.get());
    } else if (taskDatePattern.isPresent()) {
      return DateFormatUtils.formatUTC(timestamp, taskDatePattern.get());
    } else if (timeZone.isPresent()) {
      return DateFormatUtils.format(timestamp, DEFAULT_TIMESTAMP_FORMAT, timeZone.get());
    } else {
      return DateFormatUtils.format(timestamp, DEFAULT_TIMESTAMP_FORMAT);
    }
  }

  public List getJadeTaskMetadata(Collection taskMetadata) {
    List output = Lists.newArrayListWithCapacity(taskMetadata.size());

    for (SingularityTaskMetadata metadataElement : taskMetadata) {
      output.add(
          new SingularityMailTaskMetadata(
              humanizeTimestamp(metadataElement.getTimestamp()),
              metadataElement.getType(),
              metadataElement.getTitle(),
              metadataElement.getUser().or(""),
              metadataElement.getMessage().or(""),
              metadataElement.getLevel().toString()));
    }

    return output;
  }

  public List getJadeTaskHistory(Collection taskHistory) {
    List output = Lists.newArrayListWithCapacity(taskHistory.size());

    for (SingularityTaskHistoryUpdate taskUpdate : taskHistory) {
      output.add(
          new SingularityMailTaskHistoryUpdate(
              humanizeTimestamp(taskUpdate.getTimestamp()),
              WordUtils.capitalize(taskUpdate.getTaskState().getDisplayName()),
              taskUpdate.getStatusMessage().or("")));
    }

    return output;
  }

  public List getJadeDisasterStats(Collection stats) {
    List mailStats = new ArrayList<>();
    for (SingularityDisasterDataPoint stat : stats) {
      mailStats.add(new SingularityMailDisasterDataPoint(humanizeTimestamp(stat.getTimestamp()), stat));
    }
    return mailStats;
  }

  public List getTaskLogs(SingularityTaskId taskId, Optional task, Optional directory) {
    if (!smtpConfiguration.isPresent()) {
      LOG.warn("Tried to getTaskLogs for sending email without SMTP configuration set.");
      return Collections.emptyList();
    }

    List taskEmailTailFiles = smtpConfiguration.get().getTaskEmailTailFiles();
    List logTails = Lists.newArrayListWithCapacity(taskEmailTailFiles.size());

    for (String filePath : taskEmailTailFiles) {
      // To enable support for tailing the service.log file, replace instances of $MESOS_TASK_ID.
      filePath = filePath.replaceAll("\\$MESOS_TASK_ID", MesosUtils.getSafeTaskIdForDirectory(taskId.getId()));

      logTails.add(
          new SingularityMailTaskLog(
              filePath,
              getFileName(filePath),
              getSingularityLogLink(filePath, taskId.getId()),
              getTaskLogFile(taskId, filePath, task, directory).or("")));
    }

    return logTails;
  }

  private Optional getLogErrorRegex(final Optional task) {
    Optional maybeRegex;
    SMTPConfiguration configuration = smtpConfiguration.get();
    if (task.isPresent() && task.get().getTaskRequest().getRequest().getTaskLogErrorRegex().isPresent()
    && !task.get().getTaskRequest().getRequest().getTaskLogErrorRegex().get().equals("")) {
      maybeRegex = task.get().getTaskRequest().getRequest().getTaskLogErrorRegex();
    } else {
      maybeRegex = configuration.getTaskLogErrorRegex();
    }
    if (!maybeRegex.isPresent()) {
      LOG.trace("No task log error regex provided.");
      return Optional.absent();
    }
    String regex = maybeRegex.get();

    Boolean caseSensitive;
    if (task.isPresent() && task.get().getTaskRequest().getRequest().getTaskLogErrorRegexCaseSensitive().isPresent()) {
      caseSensitive = task.get().getTaskRequest().getRequest().getTaskLogErrorRegexCaseSensitive().get();
    } else if (configuration.getTaskLogErrorRegexCaseSensitive().isPresent()) {
      caseSensitive = configuration.getTaskLogErrorRegexCaseSensitive().get();
    } else {
      caseSensitive = true;
    }

    try {
      if (caseSensitive) {
        return Optional.of(Pattern.compile(regex));
      } else {
        return Optional.of(Pattern.compile(regex, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE));
      }
    } catch (PatternSyntaxException e) {
      LOG.error("Invalid task log error regex supplied: \"{}\". Received exception: {}", regex, e);
      return Optional.absent();
    }
  }

  private Optional getTaskLogFile(final SingularityTaskId taskId, final String filename, final Optional task, final Optional directory) {
    if (!smtpConfiguration.isPresent()) {
      LOG.warn("Tried to get a task log file without SMTP configuration set up.");
      return Optional.absent();
    }
    if (!task.isPresent() || !directory.isPresent()) {
      LOG.warn("Couldn't retrieve {} for {} because task ({}) or directory ({}) wasn't present", filename, taskId, task.isPresent(), directory.isPresent());
      return Optional.absent();
    }

    final String slaveHostname = task.get().getHostname();

    final String fullPath = String.format("%s/%s", directory.get(), filename);

    final Long logLength = (long) smtpConfiguration.get().getTaskLogLength();

    final Optional logChunkObject;

    LOG.trace("Getting offset (maybe) for task {} file {}", taskId.getId(), fullPath);
    Optional maybeOffset = getMaybeTaskLogReadOffset(slaveHostname, fullPath, logLength, task);

    if (!maybeOffset.isPresent()) {
      LOG.trace("Failed to find logs or error finding logs for task {} file {}", taskId.getId(), fullPath);
      return Optional.absent();
    }
    try {
      logChunkObject = sandboxManager.read(slaveHostname, fullPath, maybeOffset, Optional.of(logLength));
    } catch (RuntimeException e) {
      LOG.error("Sandboxmanager failed to read {}/{} on slave {}", directory.get(), filename, slaveHostname, e);
      return Optional.absent();
    }

    if (logChunkObject.isPresent()) {
      return Optional.of(logChunkObject.get().getData());
    } else {
      LOG.error("Failed to get {} log for {}", filename, taskId.getId());
      return Optional.absent();
    }
  }

  // Searches through the file, looking for first error
  private Optional getMaybeTaskLogReadOffset(final String slaveHostname, final String fullPath, final Long logLength, Optional task) {
    long offset = 0;
    long maxOffset = smtpConfiguration.get().getMaxTaskLogSearchOffset();
    Optional maybePattern = getLogErrorRegex(task);
    Pattern pattern;
    if (maybePattern.isPresent()) {
      pattern = maybePattern.get();
    } else {
      LOG.trace("Could not get regex pattern. Reading from bottom of file instead.");
      return getMaybeTaskLogEndOfFileOffset(slaveHostname, fullPath, logLength);
    }
    long length = logLength + pattern.toString().length(); // Get extra so that we can be sure to find the error
    Optional logChunkObject;
    Optional previous = Optional.absent();

    while (offset <= maxOffset) {
      try {
        logChunkObject = sandboxManager.read(slaveHostname, fullPath, Optional.of(offset), Optional.of(length));
      } catch (RuntimeException e) {
        LOG.error("Sandboxmanager failed to read {} on slave {}", fullPath, slaveHostname, e);
        return Optional.absent();
      }
      if (logChunkObject.isPresent()) {
        if (logChunkObject.get().getData().equals("")) { // Passed end of file
          if (previous.isPresent()) { // If there was any log, get the bottom bytes of it
            long end = previous.get().getOffset() + previous.get().getData().length();
            return Optional.of(end - logLength);
          }
          return Optional.absent();
        }
        Matcher matcher = pattern.matcher(logChunkObject.get().getData());
        if (matcher.find()) {
          return Optional.of(offset + matcher.start());
        } else {
          offset += logLength;
        }
      } else { // Couldn't read anything
        LOG.error("Failed to read log file {}", fullPath);
        return Optional.absent();
      }
      previous = logChunkObject;
    }
    LOG.trace("Searched through the first {} bytes of file {} and didn't find an error. Tailing bottom of file instead", maxOffset, fullPath);
    return getMaybeTaskLogEndOfFileOffset(slaveHostname, fullPath, logLength);
  }

  private Optional getMaybeTaskLogEndOfFileOffset(final String slaveHostname, final String fullPath, final long logLength) {
    Optional logChunkObject;
    try {
      logChunkObject = sandboxManager.read(slaveHostname, fullPath, Optional.absent(), Optional.absent());
    } catch (RuntimeException e) {
      LOG.error("Sandboxmanager failed to read {} on slave {}", fullPath, slaveHostname, e);
      return Optional.absent();
    }
    if (logChunkObject.isPresent()) {
      return Optional.of(Math.max(0, logChunkObject.get().getOffset() - logLength));
    } else {
      LOG.error("Failed to get offset for log file {}", fullPath);
      return Optional.absent();
    }

  }

  public String getFileName(String path) {
    String[] splitPath = path.split("/");
    return splitPath[splitPath.length - 1];
  }

  public String getSingularityTaskLink(String taskId) {
    if (!uiBaseUrl.isPresent()) {
      return "";
    }

    return String.format(TASK_LINK_FORMAT, uiBaseUrl.get(), taskId);
  }

  public String getSingularityRequestLink(String requestId) {
    if (!uiBaseUrl.isPresent()) {
      return "";
    }

    return String.format(REQUEST_LINK_FORMAT, uiBaseUrl.get(), requestId);
  }

  public String getSingularityLogLink(String logPath, String taskId) {
    if (!uiBaseUrl.isPresent()) {
      return "";
    }

    return String.format(LOG_LINK_FORMAT, uiBaseUrl.get(), taskId, logPath);
  }

  public String getSubjectForTaskHistory(SingularityTaskId taskId, ExtendedTaskState state, SingularityEmailType type, Collection history) {
    if (type == SingularityEmailType.TASK_SCHEDULED_OVERDUE_TO_FINISH) {
      return String.format("Task is overdue to finish (%s)", taskId.toString());
    }

    if (!didTaskRun(history)) {
      return String.format("Task never started and was %s (%s)", state.getDisplayName(), taskId.toString());
    }

    return String.format("Task %s (%s)", state.getDisplayName(), taskId.toString());
  }

  public boolean didTaskRun(Collection history) {
    SingularityTaskHistoryUpdate.SimplifiedTaskState simplifiedTaskState = SingularityTaskHistoryUpdate.getCurrentState(history);

    return simplifiedTaskState == SingularityTaskHistoryUpdate.SimplifiedTaskState.DONE || simplifiedTaskState == SingularityTaskHistoryUpdate.SimplifiedTaskState.RUNNING;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy