/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you 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 org.apache.hadoop.yarn.client.cli;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import javax.ws.rs.core.MediaType;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.classification.InterfaceAudience.Public;
import org.apache.hadoop.classification.InterfaceStability.Evolving;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.yarn.api.records.ApplicationId;
import org.apache.hadoop.yarn.api.records.ApplicationReport;
import org.apache.hadoop.yarn.api.records.ContainerReport;
import org.apache.hadoop.yarn.api.records.YarnApplicationState;
import org.apache.hadoop.yarn.client.api.YarnClient;
import org.apache.hadoop.yarn.conf.YarnConfiguration;
import org.apache.hadoop.yarn.exceptions.YarnException;
import org.apache.hadoop.yarn.logaggregation.LogCLIHelpers;
import org.apache.hadoop.yarn.util.ConverterUtils;
import org.apache.hadoop.yarn.util.Times;
import org.apache.hadoop.yarn.webapp.util.WebAppUtils;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;

import com.google.common.annotations.VisibleForTesting;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientHandlerException;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.UniformInterfaceException;
import com.sun.jersey.api.client.WebResource;

@Public
@Evolving
public class LogsCLI extends Configured implements Tool {

  private static final String CONTAINER_ID_OPTION = "containerId";
  private static final String APPLICATION_ID_OPTION = "applicationId";
  private static final String NODE_ADDRESS_OPTION = "nodeAddress";
  private static final String APP_OWNER_OPTION = "appOwner";
  private static final String AM_CONTAINER_OPTION = "am";
  private static final String CONTAINER_LOG_FILES = "logFiles";
  public static final String HELP_CMD = "help";

  @Override
  public int run(String[] args) throws Exception {

    Options opts = new Options();
    opts.addOption(HELP_CMD, false, "Displays help for all commands.");
    Option appIdOpt =
        new Option(APPLICATION_ID_OPTION, true, "ApplicationId (required)");
    appIdOpt.setRequired(true);
    opts.addOption(appIdOpt);
    opts.addOption(CONTAINER_ID_OPTION, true, "ContainerId. "
        + "By default, it will only print syslog if the application is runing."
        + " Work with -logFiles to get other logs.");
    opts.addOption(NODE_ADDRESS_OPTION, true, "NodeAddress in the format "
      + "nodename:port");
    opts.addOption(APP_OWNER_OPTION, true,
      "AppOwner (assumed to be current user if not specified)");
    Option amOption = new Option(AM_CONTAINER_OPTION, true, 
      "Prints the AM Container logs for this application. "
      + "Specify comma-separated value to get logs for related AM Container. "
      + "For example, If we specify -am 1,2, we will get the logs for "
      + "the first AM Container as well as the second AM Container. "
      + "To get logs for all AM Containers, use -am ALL. "
      + "To get logs for the latest AM Container, use -am -1. "
      + "By default, it will only print out syslog. Work with -logFiles "
      + "to get other logs");
    amOption.setValueSeparator(',');
    amOption.setArgs(Option.UNLIMITED_VALUES);
    amOption.setArgName("AM Containers");
    opts.addOption(amOption);
    Option logFileOpt = new Option(CONTAINER_LOG_FILES, true,
      "Work with -am/-containerId and specify comma-separated value "
      + "to get specified Container log files");
    logFileOpt.setValueSeparator(',');
    logFileOpt.setArgs(Option.UNLIMITED_VALUES);
    logFileOpt.setArgName("Log File Name");
    opts.addOption(logFileOpt);

    opts.getOption(APPLICATION_ID_OPTION).setArgName("Application ID");
    opts.getOption(CONTAINER_ID_OPTION).setArgName("Container ID");
    opts.getOption(NODE_ADDRESS_OPTION).setArgName("Node Address");
    opts.getOption(APP_OWNER_OPTION).setArgName("Application Owner");
    opts.getOption(AM_CONTAINER_OPTION).setArgName("AM Containers");

    Options printOpts = new Options();
    printOpts.addOption(opts.getOption(HELP_CMD));
    printOpts.addOption(opts.getOption(CONTAINER_ID_OPTION));
    printOpts.addOption(opts.getOption(NODE_ADDRESS_OPTION));
    printOpts.addOption(opts.getOption(APP_OWNER_OPTION));
    printOpts.addOption(opts.getOption(AM_CONTAINER_OPTION));
    printOpts.addOption(opts.getOption(CONTAINER_LOG_FILES));

    if (args.length < 1) {
      printHelpMessage(printOpts);
      return -1;
    }
    if (args[0].equals("-help")) {
      printHelpMessage(printOpts);
      return 0;
    }
    CommandLineParser parser = new GnuParser();
    String appIdStr = null;
    String containerIdStr = null;
    String nodeAddress = null;
    String appOwner = null;
    boolean getAMContainerLogs = false;
    String[] logFiles = null;
    List<String> amContainersList = new ArrayList<String>();
    try {
      CommandLine commandLine = parser.parse(opts, args, true);
      appIdStr = commandLine.getOptionValue(APPLICATION_ID_OPTION);
      containerIdStr = commandLine.getOptionValue(CONTAINER_ID_OPTION);
      nodeAddress = commandLine.getOptionValue(NODE_ADDRESS_OPTION);
      appOwner = commandLine.getOptionValue(APP_OWNER_OPTION);
      getAMContainerLogs = commandLine.hasOption(AM_CONTAINER_OPTION);
      if (getAMContainerLogs) {
        String[] amContainers = commandLine.getOptionValues(AM_CONTAINER_OPTION);
        for (String am : amContainers) {
          boolean errorInput = false;
          if (!am.trim().equalsIgnoreCase("ALL")) {
            try {
              int id = Integer.parseInt(am.trim());
              if (id != -1 && id <= 0) {
                errorInput = true;
              }
            } catch (NumberFormatException ex) {
              errorInput = true;
            }
            if (errorInput) {
              System.err.println(
                "Invalid input for option -am. Valid inputs are 'ALL', -1 "
                + "and any other integer which is larger than 0.");
              printHelpMessage(printOpts);
              return -1;
            }
            amContainersList.add(am.trim());
          } else {
            amContainersList.add("ALL");
            break;
          }
        }
      }
      if (commandLine.hasOption(CONTAINER_LOG_FILES)) {
        logFiles = commandLine.getOptionValues(CONTAINER_LOG_FILES);
      }
    } catch (ParseException e) {
      System.err.println("options parsing failed: " + e.getMessage());
      printHelpMessage(printOpts);
      return -1;
    }

    if (appIdStr == null) {
      System.err.println("ApplicationId cannot be null!");
      printHelpMessage(printOpts);
      return -1;
    }

    ApplicationId appId = null;
    try {
      appId = ConverterUtils.toApplicationId(appIdStr);
    } catch (Exception e) {
      System.err.println("Invalid ApplicationId specified");
      return -1;
    }

    LogCLIHelpers logCliHelper = new LogCLIHelpers();
    logCliHelper.setConf(getConf());

    if (appOwner == null || appOwner.isEmpty()) {
      appOwner = UserGroupInformation.getCurrentUser().getShortUserName();
    }

    YarnApplicationState appState = YarnApplicationState.NEW;
    try {
      appState = getApplicationState(appId);
      if (appState == YarnApplicationState.NEW
          || appState == YarnApplicationState.NEW_SAVING
          || appState == YarnApplicationState.SUBMITTED) {
        System.out.println("Logs are not avaiable right now.");
        return -1;
      }
    } catch (IOException | YarnException e) {
      System.err.println("Unable to get ApplicationState."
          + " Attempting to fetch logs directly from the filesystem.");
    }

    // To get am logs
    if (getAMContainerLogs) {
      // if we do not specify the value for CONTAINER_LOG_FILES option,
      // we will only output syslog
      if (logFiles == null || logFiles.length == 0) {
        logFiles = new String[] { "syslog" };
      }
      // If the application is running, we will call the RM WebService
      // to get the AppAttempts which includes the nodeHttpAddress
      // and containerId for all the AM Containers.
      // After that, we will call NodeManager webService to get the
      // related logs
      if (appState == YarnApplicationState.ACCEPTED
          || appState == YarnApplicationState.RUNNING) {
        return printAMContainerLogs(getConf(), appIdStr, amContainersList,
          logFiles, logCliHelper, appOwner, false);
      } else {
        // If the application is in the final state, we will call RM webservice
        // to get all AppAttempts information first. If we get nothing,
        // we will try to call AHS webservice to get related AppAttempts
        // which includes nodeAddress for the AM Containers.
        // After that, we will use nodeAddress and containerId
        // to get logs from HDFS directly.
        if (getConf().getBoolean(YarnConfiguration.APPLICATION_HISTORY_ENABLED,
          YarnConfiguration.DEFAULT_APPLICATION_HISTORY_ENABLED)) {
          return printAMContainerLogs(getConf(), appIdStr, amContainersList,
            logFiles, logCliHelper, appOwner, true);
        } else {
          System.out
            .println("Can not get AMContainers logs for the application:"
                + appId);
          System.out.println("This application:" + appId + " is finished."
              + " Please enable the application history service. Or Using "
              + "yarn logs -applicationId <appId> -containerId <containerId> "
              + "--nodeAddress <nodeHttpAddress> to get the container logs");
          return -1;
        }
      }
    }

    int resultCode = 0;
    if (containerIdStr != null) {
      // if we provide the node address and the application is in the final
      // state, we could directly get logs from HDFS.
      if (nodeAddress != null && isApplicationFinished(appState)) {
        return logCliHelper.dumpAContainersLogsForALogType(appIdStr,
            containerIdStr, nodeAddress, appOwner, logFiles == null ? null
                : Arrays.asList(logFiles));
      }
      try {
        // If the nodeAddress is not provided, we will try to get
        // the ContainerReport. In the containerReport, we could get
        // nodeAddress and nodeHttpAddress
        ContainerReport report = getContainerReport(containerIdStr);
        String nodeHttpAddress =
            report.getNodeHttpAddress().replaceFirst(
              WebAppUtils.getHttpSchemePrefix(getConf()), "");
        String nodeId = report.getAssignedNode().toString();
        // If the application is not in the final state,
        // we will provide the NodeHttpAddress and get the container logs
        // by calling NodeManager webservice.
        if (!isApplicationFinished(appState)) {
          if (logFiles == null || logFiles.length == 0) {
            logFiles = new String[] { "syslog" };
          }
          printContainerLogsFromRunningApplication(getConf(), appIdStr,
            containerIdStr, nodeHttpAddress, nodeId, logFiles, logCliHelper,
            appOwner);
        } else {
          // If the application is in the final state, we will directly
          // get the container logs from HDFS.
          printContainerLogsForFinishedApplication(appIdStr, containerIdStr,
            nodeId, logFiles, logCliHelper, appOwner);
        }
        return resultCode;
      } catch (IOException | YarnException ex) {
        System.err.println("Unable to get logs for this container:"
            + containerIdStr + "for the application:" + appId);
        if (!getConf().getBoolean(YarnConfiguration.APPLICATION_HISTORY_ENABLED,
          YarnConfiguration.DEFAULT_APPLICATION_HISTORY_ENABLED)) {
          System.out.println("Please enable the application history service. Or ");
        }
        System.out.println("Using "
            + "yarn logs -applicationId <appId> -containerId <containerId> "
            + "--nodeAddress <nodeHttpAddress> to get the container logs");
        return -1;
      }
    } else {
      if (nodeAddress == null) {
        resultCode =
            logCliHelper.dumpAllContainersLogs(appId, appOwner, System.out);
      } else {
        System.out.println("Should at least provide ContainerId!");
        printHelpMessage(printOpts);
        resultCode = -1;
      }
    }
    return resultCode;
  }

  private YarnApplicationState getApplicationState(ApplicationId appId)
      throws IOException, YarnException {
    YarnClient yarnClient = createYarnClient();

    try {
      ApplicationReport appReport = yarnClient.getApplicationReport(appId);
      return appReport.getYarnApplicationState();
    } finally {
      yarnClient.close();
    }
  }
  
  @VisibleForTesting
  protected YarnClient createYarnClient() {
    YarnClient yarnClient = YarnClient.createYarnClient();
    yarnClient.init(getConf());
    yarnClient.start();
    return yarnClient;
  }

  public static void main(String[] args) throws Exception {
    Configuration conf = new YarnConfiguration();
    LogsCLI logDumper = new LogsCLI();
    logDumper.setConf(conf);
    int exitCode = logDumper.run(args);
    System.exit(exitCode);
  }

  private void printHelpMessage(Options options) {
    System.out.println("Retrieve logs for completed YARN applications.");
    HelpFormatter formatter = new HelpFormatter();
    formatter.printHelp("yarn logs -applicationId <application ID> [OPTIONS]", new Options());
    formatter.setSyntaxPrefix("");
    formatter.printHelp("general options are:", options);
  }

  private List<JSONObject> getAMContainerInfoForRMWebService(
      Configuration conf, String appId) throws ClientHandlerException,
      UniformInterfaceException, JSONException {
    Client webServiceClient = Client.create();
    String webAppAddress =
        WebAppUtils.getWebAppBindURL(conf, YarnConfiguration.RM_BIND_HOST,
          WebAppUtils.getRMWebAppURLWithScheme(conf));
    WebResource webResource = webServiceClient.resource(webAppAddress);

    ClientResponse response =
        webResource.path("ws").path("v1").path("cluster").path("apps")
          .path(appId).path("appattempts").accept(MediaType.APPLICATION_JSON)
          .get(ClientResponse.class);
    JSONObject json =
        response.getEntity(JSONObject.class).getJSONObject("appAttempts");
    JSONArray requests = json.getJSONArray("appAttempt");
    List<JSONObject> amContainersList = new ArrayList<JSONObject>();
    for (int i = 0; i < requests.length(); i++) {
      amContainersList.add(requests.getJSONObject(i));
    }
    return amContainersList;
  }

  private List<JSONObject> getAMContainerInfoForAHSWebService(Configuration conf,
      String appId) throws ClientHandlerException, UniformInterfaceException,
      JSONException {
    Client webServiceClient = Client.create();
    String webAppAddress =
        WebAppUtils.getHttpSchemePrefix(conf)
            + WebAppUtils.getAHSWebAppURLWithoutScheme(conf);
    WebResource webResource = webServiceClient.resource(webAppAddress);

    ClientResponse response =
        webResource.path("ws").path("v1").path("applicationhistory").path("apps")
          .path(appId).path("appattempts").accept(MediaType.APPLICATION_JSON)
          .get(ClientResponse.class);
    JSONObject json = response.getEntity(JSONObject.class);
    JSONArray requests = json.getJSONArray("appAttempt");
    List<JSONObject> amContainersList = new ArrayList<JSONObject>();
    for (int i = 0; i < requests.length(); i++) {
      amContainersList.add(requests.getJSONObject(i));
    }
    Collections.reverse(amContainersList);
    return amContainersList;
  }

  private void printContainerLogsFromRunningApplication(Configuration conf,
      String appId, String containerIdStr, String nodeHttpAddress,
      String nodeId, String[] logFiles, LogCLIHelpers logCliHelper,
      String appOwner) throws IOException {
    Client webServiceClient = Client.create();
    String containerString = "\n\nContainer: " + containerIdStr;
    System.out.println(containerString);
    System.out.println(StringUtils.repeat("=", containerString.length()));
    for (String logFile : logFiles) {
      System.out.println("LogType:" + logFile);
      System.out.println("Log Upload Time:"
          + Times.format(System.currentTimeMillis()));
      System.out.println("Log Contents:");
      try {
        WebResource webResource =
            webServiceClient.resource(WebAppUtils.getHttpSchemePrefix(conf)
                + nodeHttpAddress);
        ClientResponse response =
            webResource.path("ws").path("v1").path("node")
              .path("containerlogs").path(containerIdStr).path(logFile)
              .accept(MediaType.TEXT_PLAIN).get(ClientResponse.class);
        System.out.println(response.getEntity(String.class));
        System.out.println("End of LogType:" + logFile);
      } catch (ClientHandlerException | UniformInterfaceException ex) {
        System.out.println("Can not find the log file:" + logFile
            + " for the container:" + containerIdStr + " in NodeManager:"
            + nodeId);
      }
    }
    // for the case, we have already uploaded partial logs in HDFS
    logCliHelper.dumpAContainersLogsForALogType(appId, containerIdStr, nodeId,
      appOwner, Arrays.asList(logFiles));
  }

  private void printContainerLogsForFinishedApplication(String appId,
      String containerId, String nodeAddress, String[] logFiles,
      LogCLIHelpers logCliHelper, String appOwner) throws IOException {
    String containerString = "\n\nContainer: " + containerId;
    System.out.println(containerString);
    System.out.println(StringUtils.repeat("=", containerString.length()));
    logCliHelper.dumpAContainersLogsForALogType(appId, containerId,
      nodeAddress, appOwner, logFiles != null ? Arrays.asList(logFiles) : null);
  }

  private ContainerReport getContainerReport(String containerIdStr)
      throws YarnException, IOException {
    YarnClient yarnClient = createYarnClient();
    try {
      return yarnClient.getContainerReport(ConverterUtils
        .toContainerId(containerIdStr));
    } finally {
      yarnClient.close();
    }
  }

  private boolean isApplicationFinished(YarnApplicationState appState) {
    return appState == YarnApplicationState.FINISHED
        || appState == YarnApplicationState.FAILED
        || appState == YarnApplicationState.KILLED; 
  }

  private int printAMContainerLogs(Configuration conf, String appId,
      List<String> amContainers, String[] logFiles, LogCLIHelpers logCliHelper,
      String appOwner, boolean applicationFinished) throws Exception {
    List<JSONObject> amContainersList = null;
    List<AMLogsRequest> requests = new ArrayList<AMLogsRequest>();
    boolean getAMContainerLists = false;
    String errorMessage = "";
    try {
      amContainersList = getAMContainerInfoForRMWebService(conf, appId);
      if (amContainersList != null && !amContainersList.isEmpty()) {
        getAMContainerLists = true;
        for (JSONObject amContainer : amContainersList) {
          AMLogsRequest request = new AMLogsRequest(applicationFinished);
          request.setAmContainerId(amContainer.getString("containerId"));
          request.setNodeHttpAddress(amContainer.getString("nodeHttpAddress"));
          request.setNodeId(amContainer.getString("nodeId"));
          requests.add(request);
        }
      }
    } catch (Exception ex) {
      errorMessage = ex.getMessage();
      if (applicationFinished) {
        try {
          amContainersList = getAMContainerInfoForAHSWebService(conf, appId);
          if (amContainersList != null && !amContainersList.isEmpty()) {
            getAMContainerLists = true;
            for (JSONObject amContainer : amContainersList) {
              AMLogsRequest request = new AMLogsRequest(applicationFinished);
              request.setAmContainerId(amContainer.getString("amContainerId"));
              requests.add(request);
            }
          }
        } catch (Exception e) {
          errorMessage = e.getMessage();
        }
      }
    }

    if (!getAMContainerLists) {
      System.err.println("Unable to get AM container informations "
          + "for the application:" + appId);
      System.err.println(errorMessage);
      return -1;
    }

    if (amContainers.contains("ALL")) {
      for (AMLogsRequest request : requests) {
        outputAMContainerLogs(request, conf, appId, logFiles, logCliHelper,
          appOwner);
      }
      System.out.println();      
      System.out.println("Specified ALL for -am option. "
          + "Printed logs for all am containers.");
    } else {
      for (String amContainer : amContainers) {
        int amContainerId = Integer.parseInt(amContainer.trim());
        if (amContainerId == -1) {
          outputAMContainerLogs(requests.get(requests.size() - 1), conf, appId,
            logFiles, logCliHelper, appOwner);
        } else {
          if (amContainerId <= requests.size()) {
            outputAMContainerLogs(requests.get(amContainerId - 1), conf, appId,
              logFiles, logCliHelper, appOwner);
          }
        }
      }
    }
    return 0;
  }

  private void outputAMContainerLogs(AMLogsRequest request, Configuration conf,
      String appId, String[] logFiles, LogCLIHelpers logCliHelper,
      String appOwner) throws Exception {
    String nodeHttpAddress = request.getNodeHttpAddress();
    String containerId = request.getAmContainerId();
    String nodeId = request.getNodeId();

    if (request.isAppFinished()) {
      if (containerId != null && !containerId.isEmpty()) {
        if (nodeId == null || nodeId.isEmpty()) {
          try {
            nodeId =
                getContainerReport(containerId).getAssignedNode().toString();
          } catch (Exception ex) {
            System.err.println(ex);
            nodeId = null;
          }
        }
        if (nodeId != null && !nodeId.isEmpty()) {
          printContainerLogsForFinishedApplication(appId, containerId, nodeId,
            logFiles, logCliHelper, appOwner);
        }
      }
    } else {
      if (nodeHttpAddress != null && containerId != null
          && !nodeHttpAddress.isEmpty() && !containerId.isEmpty()) {
        printContainerLogsFromRunningApplication(conf, appId, containerId,
          nodeHttpAddress, nodeId, logFiles, logCliHelper, appOwner);
      }
    }
  }

  private static class AMLogsRequest {
    private String amContainerId;
    private String nodeId;
    private String nodeHttpAddress;
    private final boolean isAppFinished;

    AMLogsRequest(boolean isAppFinished) {
      this.isAppFinished = isAppFinished;
      this.setAmContainerId("");
      this.setNodeId("");
      this.setNodeHttpAddress("");
    }

    public String getAmContainerId() {
      return amContainerId;
    }

    public void setAmContainerId(String amContainerId) {
      this.amContainerId = amContainerId;
    }

    public String getNodeId() {
      return nodeId;
    }

    public void setNodeId(String nodeId) {
      this.nodeId = nodeId;
    }

    public String getNodeHttpAddress() {
      return nodeHttpAddress;
    }

    public void setNodeHttpAddress(String nodeHttpAddress) {
      this.nodeHttpAddress = nodeHttpAddress;
    }

    public boolean isAppFinished() {
      return isAppFinished;
    }
  }
}
