/** * 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.logaggregation; import java.io.DataInput; import java.io.DataInputStream; import java.io.DataOutput; import java.io.DataOutputStream; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.io.Writer; import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.apache.commons.io.input.BoundedInputStream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.classification.InterfaceAudience.Private; import org.apache.hadoop.classification.InterfaceAudience.Public; import org.apache.hadoop.classification.InterfaceStability.Evolving; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.CreateFlag; import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FSDataOutputStream; import org.apache.hadoop.fs.FileContext; import org.apache.hadoop.fs.Options; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.permission.FsPermission; import org.apache.hadoop.io.SecureIOUtils; import org.apache.hadoop.io.Writable; import org.apache.hadoop.io.file.tfile.TFile; import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.yarn.api.records.ApplicationAccessType; import org.apache.hadoop.yarn.api.records.ContainerId; import org.apache.hadoop.yarn.conf.YarnConfiguration; import org.apache.hadoop.yarn.exceptions.YarnRuntimeException; import org.apache.hadoop.yarn.util.ConverterUtils; @Public @Evolving public class AggregatedLogFormat { private static final Log LOG = LogFactory.getLog(AggregatedLogFormat.class); private static final LogKey APPLICATION_ACL_KEY = new LogKey("APPLICATION_ACL"); private static final LogKey APPLICATION_OWNER_KEY = new LogKey("APPLICATION_OWNER"); private static final LogKey VERSION_KEY = new LogKey("VERSION"); private static final Map<String, LogKey> RESERVED_KEYS; //Maybe write out the retention policy. //Maybe write out a list of containerLogs skipped by the retention policy. private static final int VERSION = 1; /** * Umask for the log file. */ private static final FsPermission APP_LOG_FILE_UMASK = FsPermission .createImmutable((short) (0640 ^ 0777)); static { RESERVED_KEYS = new HashMap<String, AggregatedLogFormat.LogKey>(); RESERVED_KEYS.put(APPLICATION_ACL_KEY.toString(), APPLICATION_ACL_KEY); RESERVED_KEYS.put(APPLICATION_OWNER_KEY.toString(), APPLICATION_OWNER_KEY); RESERVED_KEYS.put(VERSION_KEY.toString(), VERSION_KEY); } @Public public static class LogKey implements Writable { private String keyString; public LogKey() { } public LogKey(ContainerId containerId) { this.keyString = containerId.toString(); } public LogKey(String keyString) { this.keyString = keyString; } @Override public int hashCode() { return keyString == null ? 0 : keyString.hashCode(); } @Override public boolean equals(Object obj) { if (obj instanceof LogKey) { LogKey other = (LogKey) obj; if (this.keyString == null) { return other.keyString == null; } return this.keyString.equals(other.keyString); } return false; } @Private @Override public void write(DataOutput out) throws IOException { out.writeUTF(this.keyString); } @Private @Override public void readFields(DataInput in) throws IOException { this.keyString = in.readUTF(); } @Override public String toString() { return this.keyString; } } @Private public static class LogValue { private final List<String> rootLogDirs; private final ContainerId containerId; private final String user; // TODO Maybe add a version string here. Instead of changing the version of // the entire k-v format public LogValue(List<String> rootLogDirs, ContainerId containerId, String user) { this.rootLogDirs = new ArrayList<String>(rootLogDirs); this.containerId = containerId; this.user = user; // Ensure logs are processed in lexical order Collections.sort(this.rootLogDirs); } public void write(DataOutputStream out) throws IOException { for (String rootLogDir : this.rootLogDirs) { File appLogDir = new File(rootLogDir, ConverterUtils.toString( this.containerId.getApplicationAttemptId(). getApplicationId()) ); File containerLogDir = new File(appLogDir, ConverterUtils.toString(this.containerId)); if (!containerLogDir.isDirectory()) { continue; // ContainerDir may have been deleted by the user. } // Write out log files in lexical order File[] logFiles = containerLogDir.listFiles(); Arrays.sort(logFiles); for (File logFile : logFiles) { // Write the logFile Type out.writeUTF(logFile.getName()); // Write the log length as UTF so that it is printable out.writeUTF(String.valueOf(logFile.length())); // Write the log itself FileInputStream in = null; try { in = SecureIOUtils.openForRead(logFile, getUser(), null); byte[] buf = new byte[65535]; int len = 0; while ((len = in.read(buf)) != -1) { out.write(buf, 0, len); } } catch (IOException e) { String message = "Error aggregating log file. Log file : " + logFile.getAbsolutePath() + e.getMessage(); LOG.error(message, e); out.write(message.getBytes()); } finally { if (in != null) { in.close(); } } } } } // Added for testing purpose. public String getUser() { return user; } } /** * The writer that writes out the aggregated logs. */ @Private public static class LogWriter { private final FSDataOutputStream fsDataOStream; private final TFile.Writer writer; public LogWriter(final Configuration conf, final Path remoteAppLogFile, UserGroupInformation userUgi) throws IOException { try { this.fsDataOStream = userUgi.doAs(new PrivilegedExceptionAction<FSDataOutputStream>() { @Override public FSDataOutputStream run() throws Exception { FileContext fc = FileContext.getFileContext(conf); fc.setUMask(APP_LOG_FILE_UMASK); return fc.create( remoteAppLogFile, EnumSet.of(CreateFlag.CREATE, CreateFlag.OVERWRITE), new Options.CreateOpts[] {}); } }); } catch (InterruptedException e) { throw new IOException(e); } // Keys are not sorted: null arg // 256KB minBlockSize : Expected log size for each container too this.writer = new TFile.Writer(this.fsDataOStream, 256 * 1024, conf.get( YarnConfiguration.NM_LOG_AGG_COMPRESSION_TYPE, YarnConfiguration.DEFAULT_NM_LOG_AGG_COMPRESSION_TYPE), null, conf); //Write the version string writeVersion(); } private void writeVersion() throws IOException { DataOutputStream out = this.writer.prepareAppendKey(-1); VERSION_KEY.write(out); out.close(); out = this.writer.prepareAppendValue(-1); out.writeInt(VERSION); out.close(); } public void writeApplicationOwner(String user) throws IOException { DataOutputStream out = this.writer.prepareAppendKey(-1); APPLICATION_OWNER_KEY.write(out); out.close(); out = this.writer.prepareAppendValue(-1); out.writeUTF(user); out.close(); } public void writeApplicationACLs(Map<ApplicationAccessType, String> appAcls) throws IOException { DataOutputStream out = this.writer.prepareAppendKey(-1); APPLICATION_ACL_KEY.write(out); out.close(); out = this.writer.prepareAppendValue(-1); for (Entry<ApplicationAccessType, String> entry : appAcls.entrySet()) { out.writeUTF(entry.getKey().toString()); out.writeUTF(entry.getValue()); } out.close(); } public void append(LogKey logKey, LogValue logValue) throws IOException { DataOutputStream out = this.writer.prepareAppendKey(-1); logKey.write(out); out.close(); out = this.writer.prepareAppendValue(-1); logValue.write(out); out.close(); } public void closeWriter() { try { this.writer.close(); } catch (IOException e) { LOG.warn("Exception closing writer", e); } try { this.fsDataOStream.close(); } catch (IOException e) { LOG.warn("Exception closing output-stream", e); } } } @Public @Evolving public static class LogReader { private final FSDataInputStream fsDataIStream; private final TFile.Reader.Scanner scanner; private final TFile.Reader reader; public LogReader(Configuration conf, Path remoteAppLogFile) throws IOException { FileContext fileContext = FileContext.getFileContext(conf); this.fsDataIStream = fileContext.open(remoteAppLogFile); reader = new TFile.Reader(this.fsDataIStream, fileContext.getFileStatus( remoteAppLogFile).getLen(), conf); this.scanner = reader.createScanner(); } private boolean atBeginning = true; /** * Returns the owner of the application. * * @return the application owner. * @throws IOException */ public String getApplicationOwner() throws IOException { TFile.Reader.Scanner ownerScanner = reader.createScanner(); LogKey key = new LogKey(); while (!ownerScanner.atEnd()) { TFile.Reader.Scanner.Entry entry = ownerScanner.entry(); key.readFields(entry.getKeyStream()); if (key.toString().equals(APPLICATION_OWNER_KEY.toString())) { DataInputStream valueStream = entry.getValueStream(); return valueStream.readUTF(); } ownerScanner.advance(); } return null; } /** * Returns ACLs for the application. An empty map is returned if no ACLs are * found. * * @return a map of the Application ACLs. * @throws IOException */ public Map<ApplicationAccessType, String> getApplicationAcls() throws IOException { // TODO Seek directly to the key once a comparator is specified. TFile.Reader.Scanner aclScanner = reader.createScanner(); LogKey key = new LogKey(); Map<ApplicationAccessType, String> acls = new HashMap<ApplicationAccessType, String>(); while (!aclScanner.atEnd()) { TFile.Reader.Scanner.Entry entry = aclScanner.entry(); key.readFields(entry.getKeyStream()); if (key.toString().equals(APPLICATION_ACL_KEY.toString())) { DataInputStream valueStream = entry.getValueStream(); while (true) { String appAccessOp = null; String aclString = null; try { appAccessOp = valueStream.readUTF(); } catch (EOFException e) { // Valid end of stream. break; } try { aclString = valueStream.readUTF(); } catch (EOFException e) { throw new YarnRuntimeException("Error reading ACLs", e); } acls.put(ApplicationAccessType.valueOf(appAccessOp), aclString); } } aclScanner.advance(); } return acls; } /** * Read the next key and return the value-stream. * * @param key * @return the valueStream if there are more keys or null otherwise. * @throws IOException */ public DataInputStream next(LogKey key) throws IOException { if (!this.atBeginning) { this.scanner.advance(); } else { this.atBeginning = false; } if (this.scanner.atEnd()) { return null; } TFile.Reader.Scanner.Entry entry = this.scanner.entry(); key.readFields(entry.getKeyStream()); // Skip META keys if (RESERVED_KEYS.containsKey(key.toString())) { return next(key); } DataInputStream valueStream = entry.getValueStream(); return valueStream; } /** * Get a ContainerLogsReader to read the logs for * the specified container. * * @param containerId * @return object to read the container's logs or null if the * logs could not be found * @throws IOException */ @Private public ContainerLogsReader getContainerLogsReader( ContainerId containerId) throws IOException { ContainerLogsReader logReader = null; final LogKey containerKey = new LogKey(containerId); LogKey key = new LogKey(); DataInputStream valueStream = next(key); while (valueStream != null && !key.equals(containerKey)) { valueStream = next(key); } if (valueStream != null) { logReader = new ContainerLogsReader(valueStream); } return logReader; } //TODO Change Log format and interfaces to be containerId specific. // Avoid returning completeValueStreams. // public List<String> getTypesForContainer(DataInputStream valueStream){} // // /** // * @param valueStream // * The Log stream for the container. // * @param fileType // * the log type required. // * @return An InputStreamReader for the required log type or null if the // * type is not found. // * @throws IOException // */ // public InputStreamReader getLogStreamForType(DataInputStream valueStream, // String fileType) throws IOException { // valueStream.reset(); // try { // while (true) { // String ft = valueStream.readUTF(); // String fileLengthStr = valueStream.readUTF(); // long fileLength = Long.parseLong(fileLengthStr); // if (ft.equals(fileType)) { // BoundedInputStream bis = // new BoundedInputStream(valueStream, fileLength); // return new InputStreamReader(bis); // } else { // long totalSkipped = 0; // long currSkipped = 0; // while (currSkipped != -1 && totalSkipped < fileLength) { // currSkipped = valueStream.skip(fileLength - totalSkipped); // totalSkipped += currSkipped; // } // // TODO Verify skip behaviour. // if (currSkipped == -1) { // return null; // } // } // } // } catch (EOFException e) { // return null; // } // } /** * Writes all logs for a single container to the provided writer. * @param valueStream * @param writer * @throws IOException */ public static void readAcontainerLogs(DataInputStream valueStream, Writer writer) throws IOException { int bufferSize = 65536; char[] cbuf = new char[bufferSize]; String fileType; String fileLengthStr; long fileLength; while (true) { try { fileType = valueStream.readUTF(); } catch (EOFException e) { // EndOfFile return; } fileLengthStr = valueStream.readUTF(); fileLength = Long.parseLong(fileLengthStr); writer.write("\n\nLogType:"); writer.write(fileType); writer.write("\nLogLength:"); writer.write(fileLengthStr); writer.write("\nLog Contents:\n"); // ByteLevel BoundedInputStream bis = new BoundedInputStream(valueStream, fileLength); InputStreamReader reader = new InputStreamReader(bis); int currentRead = 0; int totalRead = 0; while ((currentRead = reader.read(cbuf, 0, bufferSize)) != -1) { writer.write(cbuf, 0, currentRead); totalRead += currentRead; } } } /** * Keep calling this till you get a {@link EOFException} for getting logs of * all types for a single container. * * @param valueStream * @param out * @throws IOException */ public static void readAContainerLogsForALogType( DataInputStream valueStream, PrintStream out) throws IOException { byte[] buf = new byte[65535]; String fileType = valueStream.readUTF(); String fileLengthStr = valueStream.readUTF(); long fileLength = Long.parseLong(fileLengthStr); out.print("LogType: "); out.println(fileType); out.print("LogLength: "); out.println(fileLengthStr); out.println("Log Contents:"); int curRead = 0; long pendingRead = fileLength - curRead; int toRead = pendingRead > buf.length ? buf.length : (int) pendingRead; int len = valueStream.read(buf, 0, toRead); while (len != -1 && curRead < fileLength) { out.write(buf, 0, len); curRead += len; pendingRead = fileLength - curRead; toRead = pendingRead > buf.length ? buf.length : (int) pendingRead; len = valueStream.read(buf, 0, toRead); } out.println(""); } public void close() throws IOException { this.scanner.close(); this.fsDataIStream.close(); } } @Private public static class ContainerLogsReader { private DataInputStream valueStream; private String currentLogType = null; private long currentLogLength = 0; private BoundedInputStream currentLogData = null; private InputStreamReader currentLogISR; public ContainerLogsReader(DataInputStream stream) { valueStream = stream; } public String nextLog() throws IOException { if (currentLogData != null && currentLogLength > 0) { // seek to the end of the current log, relying on BoundedInputStream // to prevent seeking past the end of the current log do { if (currentLogData.skip(currentLogLength) < 0) { break; } } while (currentLogData.read() != -1); } currentLogType = null; currentLogLength = 0; currentLogData = null; currentLogISR = null; try { String logType = valueStream.readUTF(); String logLengthStr = valueStream.readUTF(); currentLogLength = Long.parseLong(logLengthStr); currentLogData = new BoundedInputStream(valueStream, currentLogLength); currentLogData.setPropagateClose(false); currentLogISR = new InputStreamReader(currentLogData); currentLogType = logType; } catch (EOFException e) { } return currentLogType; } public String getCurrentLogType() { return currentLogType; } public long getCurrentLogLength() { return currentLogLength; } public long skip(long n) throws IOException { return currentLogData.skip(n); } public int read(byte[] buf, int off, int len) throws IOException { return currentLogData.read(buf, off, len); } public int read(char[] buf, int off, int len) throws IOException { return currentLogISR.read(buf, off, len); } } }