1 /**
2  * Licensed to the Apache Software Foundation (ASF) under one
3  * or more contributor license agreements.  See the NOTICE file
4  * distributed with this work for additional information
5  * regarding copyright ownership.  The ASF licenses this file
6  * to you under the Apache License, Version 2.0 (the
7  * "License"); you may not use this file except in compliance
8  * with the License.  You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */
18 
19 package org.apache.hadoop.mapred;
20 
21 import java.io.BufferedOutputStream;
22 import java.io.BufferedReader;
23 import java.io.DataOutputStream;
24 import java.io.File;
25 import java.io.FileInputStream;
26 import java.io.Flushable;
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.io.InputStreamReader;
30 import java.util.ArrayList;
31 import java.util.Enumeration;
32 import java.util.List;
33 import java.util.concurrent.Executors;
34 import java.util.concurrent.ScheduledExecutorService;
35 import java.util.concurrent.ThreadFactory;
36 import java.util.concurrent.TimeUnit;
37 
38 import org.apache.commons.logging.Log;
39 import org.apache.commons.logging.LogFactory;
40 import org.apache.hadoop.classification.InterfaceAudience;
41 import org.apache.hadoop.conf.Configuration;
42 import org.apache.hadoop.fs.FileStatus;
43 import org.apache.hadoop.fs.FileSystem;
44 import org.apache.hadoop.fs.FileUtil;
45 import org.apache.hadoop.fs.LocalFileSystem;
46 import org.apache.hadoop.fs.Path;
47 import org.apache.hadoop.io.IOUtils;
48 import org.apache.hadoop.io.SecureIOUtils;
49 import org.apache.hadoop.mapreduce.JobID;
50 import org.apache.hadoop.mapreduce.util.ProcessTree;
51 import org.apache.hadoop.util.Shell;
52 import org.apache.hadoop.util.StringUtils;
53 import org.apache.hadoop.util.ShutdownHookManager;
54 import org.apache.hadoop.yarn.conf.YarnConfiguration;
55 import org.apache.log4j.Appender;
56 import org.apache.log4j.LogManager;
57 import org.apache.log4j.Logger;
58 
59 import com.google.common.base.Charsets;
60 
61 /**
62  * A simple logger to handle the task-specific user logs.
63  * This class uses the system property <code>hadoop.log.dir</code>.
64  *
65  */
66 @InterfaceAudience.Private
67 public class TaskLog {
68   private static final Log LOG =
69     LogFactory.getLog(TaskLog.class);
70 
71   static final String USERLOGS_DIR_NAME = "userlogs";
72 
73   private static final File LOG_DIR =
74     new File(getBaseLogDir(), USERLOGS_DIR_NAME).getAbsoluteFile();
75 
76   // localFS is set in (and used by) writeToIndexFile()
77   static LocalFileSystem localFS = null;
78 
getMRv2LogDir()79   public static String getMRv2LogDir() {
80     return System.getProperty(YarnConfiguration.YARN_APP_CONTAINER_LOG_DIR);
81   }
82 
getTaskLogFile(TaskAttemptID taskid, boolean isCleanup, LogName filter)83   public static File getTaskLogFile(TaskAttemptID taskid, boolean isCleanup,
84       LogName filter) {
85     if (getMRv2LogDir() != null) {
86       return new File(getMRv2LogDir(), filter.toString());
87     } else {
88       return new File(getAttemptDir(taskid, isCleanup), filter.toString());
89     }
90   }
91 
getRealTaskLogFileLocation(TaskAttemptID taskid, boolean isCleanup, LogName filter)92   static File getRealTaskLogFileLocation(TaskAttemptID taskid,
93       boolean isCleanup, LogName filter) {
94     LogFileDetail l;
95     try {
96       l = getLogFileDetail(taskid, filter, isCleanup);
97     } catch (IOException ie) {
98       LOG.error("getTaskLogFileDetail threw an exception " + ie);
99       return null;
100     }
101     return new File(l.location, filter.toString());
102   }
103   private static class LogFileDetail {
104     final static String LOCATION = "LOG_DIR:";
105     String location;
106     long start;
107     long length;
108   }
109 
getLogFileDetail(TaskAttemptID taskid, LogName filter, boolean isCleanup)110   private static LogFileDetail getLogFileDetail(TaskAttemptID taskid,
111                                                 LogName filter,
112                                                 boolean isCleanup)
113   throws IOException {
114     File indexFile = getIndexFile(taskid, isCleanup);
115     BufferedReader fis = new BufferedReader(new InputStreamReader(
116       SecureIOUtils.openForRead(indexFile, obtainLogDirOwner(taskid), null),
117       Charsets.UTF_8));
118     //the format of the index file is
119     //LOG_DIR: <the dir where the task logs are really stored>
120     //stdout:<start-offset in the stdout file> <length>
121     //stderr:<start-offset in the stderr file> <length>
122     //syslog:<start-offset in the syslog file> <length>
123     LogFileDetail l = new LogFileDetail();
124     String str = null;
125     try {
126       str = fis.readLine();
127       if (str == null) { // the file doesn't have anything
128         throw new IOException("Index file for the log of " + taskid
129             + " doesn't exist.");
130       }
131       l.location = str.substring(str.indexOf(LogFileDetail.LOCATION)
132           + LogFileDetail.LOCATION.length());
133       // special cases are the debugout and profile.out files. They are
134       // guaranteed
135       // to be associated with each task attempt since jvm reuse is disabled
136       // when profiling/debugging is enabled
137       if (filter.equals(LogName.DEBUGOUT) || filter.equals(LogName.PROFILE)) {
138         l.length = new File(l.location, filter.toString()).length();
139         l.start = 0;
140         fis.close();
141         return l;
142       }
143       str = fis.readLine();
144       while (str != null) {
145         // look for the exact line containing the logname
146         if (str.contains(filter.toString())) {
147           str = str.substring(filter.toString().length() + 1);
148           String[] startAndLen = str.split(" ");
149           l.start = Long.parseLong(startAndLen[0]);
150           l.length = Long.parseLong(startAndLen[1]);
151           break;
152         }
153         str = fis.readLine();
154       }
155       fis.close();
156       fis = null;
157     } finally {
158       IOUtils.cleanup(LOG, fis);
159     }
160     return l;
161   }
162 
getTmpIndexFile(TaskAttemptID taskid, boolean isCleanup)163   private static File getTmpIndexFile(TaskAttemptID taskid, boolean isCleanup) {
164     return new File(getAttemptDir(taskid, isCleanup), "log.tmp");
165   }
166 
getIndexFile(TaskAttemptID taskid, boolean isCleanup)167   static File getIndexFile(TaskAttemptID taskid, boolean isCleanup) {
168     return new File(getAttemptDir(taskid, isCleanup), "log.index");
169   }
170 
171   /**
172    * Obtain the owner of the log dir. This is
173    * determined by checking the job's log directory.
174    */
obtainLogDirOwner(TaskAttemptID taskid)175   static String obtainLogDirOwner(TaskAttemptID taskid) throws IOException {
176     Configuration conf = new Configuration();
177     FileSystem raw = FileSystem.getLocal(conf).getRaw();
178     Path jobLogDir = new Path(getJobDir(taskid.getJobID()).getAbsolutePath());
179     FileStatus jobStat = raw.getFileStatus(jobLogDir);
180     return jobStat.getOwner();
181   }
182 
getBaseLogDir()183   static String getBaseLogDir() {
184     return System.getProperty("hadoop.log.dir");
185   }
186 
getAttemptDir(TaskAttemptID taskid, boolean isCleanup)187   static File getAttemptDir(TaskAttemptID taskid, boolean isCleanup) {
188     String cleanupSuffix = isCleanup ? ".cleanup" : "";
189     return new File(getJobDir(taskid.getJobID()), taskid + cleanupSuffix);
190   }
191   private static long prevOutLength;
192   private static long prevErrLength;
193   private static long prevLogLength;
194 
195   private static synchronized
writeToIndexFile(String logLocation, boolean isCleanup)196   void writeToIndexFile(String logLocation,
197                         boolean isCleanup) throws IOException {
198     // To ensure atomicity of updates to index file, write to temporary index
199     // file first and then rename.
200     File tmpIndexFile = getTmpIndexFile(currentTaskid, isCleanup);
201 
202     BufferedOutputStream bos = null;
203     DataOutputStream dos = null;
204     try{
205       bos = new BufferedOutputStream(
206           SecureIOUtils.createForWrite(tmpIndexFile, 0644));
207       dos = new DataOutputStream(bos);
208       //the format of the index file is
209       //LOG_DIR: <the dir where the task logs are really stored>
210       //STDOUT: <start-offset in the stdout file> <length>
211       //STDERR: <start-offset in the stderr file> <length>
212       //SYSLOG: <start-offset in the syslog file> <length>
213 
214       dos.writeBytes(LogFileDetail.LOCATION + logLocation + "\n"
215           + LogName.STDOUT.toString() + ":");
216       dos.writeBytes(Long.toString(prevOutLength) + " ");
217       dos.writeBytes(Long.toString(new File(logLocation, LogName.STDOUT
218           .toString()).length() - prevOutLength)
219           + "\n" + LogName.STDERR + ":");
220       dos.writeBytes(Long.toString(prevErrLength) + " ");
221       dos.writeBytes(Long.toString(new File(logLocation, LogName.STDERR
222           .toString()).length() - prevErrLength)
223           + "\n" + LogName.SYSLOG.toString() + ":");
224       dos.writeBytes(Long.toString(prevLogLength) + " ");
225       dos.writeBytes(Long.toString(new File(logLocation, LogName.SYSLOG
226           .toString()).length() - prevLogLength)
227           + "\n");
228       dos.close();
229       dos = null;
230       bos.close();
231       bos = null;
232     } finally {
233       IOUtils.cleanup(LOG, dos, bos);
234     }
235 
236     File indexFile = getIndexFile(currentTaskid, isCleanup);
237     Path indexFilePath = new Path(indexFile.getAbsolutePath());
238     Path tmpIndexFilePath = new Path(tmpIndexFile.getAbsolutePath());
239 
240     if (localFS == null) {// set localFS once
241       localFS = FileSystem.getLocal(new Configuration());
242     }
243     localFS.rename (tmpIndexFilePath, indexFilePath);
244   }
resetPrevLengths(String logLocation)245   private static void resetPrevLengths(String logLocation) {
246     prevOutLength = new File(logLocation, LogName.STDOUT.toString()).length();
247     prevErrLength = new File(logLocation, LogName.STDERR.toString()).length();
248     prevLogLength = new File(logLocation, LogName.SYSLOG.toString()).length();
249   }
250   private volatile static TaskAttemptID currentTaskid = null;
251 
252   @SuppressWarnings("unchecked")
syncLogs(String logLocation, TaskAttemptID taskid, boolean isCleanup)253   public synchronized static void syncLogs(String logLocation,
254                                            TaskAttemptID taskid,
255                                            boolean isCleanup)
256   throws IOException {
257     System.out.flush();
258     System.err.flush();
259     Enumeration<Logger> allLoggers = LogManager.getCurrentLoggers();
260     while (allLoggers.hasMoreElements()) {
261       Logger l = allLoggers.nextElement();
262       Enumeration<Appender> allAppenders = l.getAllAppenders();
263       while (allAppenders.hasMoreElements()) {
264         Appender a = allAppenders.nextElement();
265         if (a instanceof TaskLogAppender) {
266           ((TaskLogAppender)a).flush();
267         }
268       }
269     }
270     if (currentTaskid != taskid) {
271       currentTaskid = taskid;
272       resetPrevLengths(logLocation);
273     }
274     writeToIndexFile(logLocation, isCleanup);
275   }
276 
syncLogsShutdown( ScheduledExecutorService scheduler)277   public static synchronized void syncLogsShutdown(
278     ScheduledExecutorService scheduler)
279   {
280     // flush standard streams
281     //
282     System.out.flush();
283     System.err.flush();
284 
285     if (scheduler != null) {
286       scheduler.shutdownNow();
287     }
288 
289     // flush & close all appenders
290     LogManager.shutdown();
291   }
292 
293   @SuppressWarnings("unchecked")
syncLogs()294   public static synchronized void syncLogs() {
295     // flush standard streams
296     //
297     System.out.flush();
298     System.err.flush();
299 
300     // flush flushable appenders
301     //
302     final Logger rootLogger = Logger.getRootLogger();
303     flushAppenders(rootLogger);
304     final Enumeration<Logger> allLoggers = rootLogger.getLoggerRepository().
305       getCurrentLoggers();
306     while (allLoggers.hasMoreElements()) {
307       final Logger l = allLoggers.nextElement();
308       flushAppenders(l);
309     }
310   }
311 
312   @SuppressWarnings("unchecked")
flushAppenders(Logger l)313   private static void flushAppenders(Logger l) {
314     final Enumeration<Appender> allAppenders = l.getAllAppenders();
315     while (allAppenders.hasMoreElements()) {
316       final Appender a = allAppenders.nextElement();
317       if (a instanceof Flushable) {
318         try {
319           ((Flushable) a).flush();
320         } catch (IOException ioe) {
321           System.err.println(a + ": Failed to flush!"
322             + StringUtils.stringifyException(ioe));
323         }
324       }
325     }
326   }
327 
createLogSyncer()328   public static ScheduledExecutorService createLogSyncer() {
329     final ScheduledExecutorService scheduler =
330       Executors.newSingleThreadScheduledExecutor(
331         new ThreadFactory() {
332           @Override
333           public Thread newThread(Runnable r) {
334             final Thread t = Executors.defaultThreadFactory().newThread(r);
335             t.setDaemon(true);
336             t.setName("Thread for syncLogs");
337             return t;
338           }
339         });
340     ShutdownHookManager.get().addShutdownHook(new Runnable() {
341         @Override
342         public void run() {
343           TaskLog.syncLogsShutdown(scheduler);
344         }
345       }, 50);
346     scheduler.scheduleWithFixedDelay(
347         new Runnable() {
348           @Override
349           public void run() {
350             TaskLog.syncLogs();
351           }
352         }, 0L, 5L, TimeUnit.SECONDS);
353     return scheduler;
354   }
355 
356   /**
357    * The filter for userlogs.
358    */
359   @InterfaceAudience.Private
360   public static enum LogName {
361     /** Log on the stdout of the task. */
362     STDOUT ("stdout"),
363 
364     /** Log on the stderr of the task. */
365     STDERR ("stderr"),
366 
367     /** Log on the map-reduce system logs of the task. */
368     SYSLOG ("syslog"),
369 
370     /** The java profiler information. */
371     PROFILE ("profile.out"),
372 
373     /** Log the debug script's stdout  */
374     DEBUGOUT ("debugout");
375 
376     private String prefix;
377 
LogName(String prefix)378     private LogName(String prefix) {
379       this.prefix = prefix;
380     }
381 
382     @Override
toString()383     public String toString() {
384       return prefix;
385     }
386   }
387 
388   public static class Reader extends InputStream {
389     private long bytesRemaining;
390     private FileInputStream file;
391 
392     /**
393      * Read a log file from start to end positions. The offsets may be negative,
394      * in which case they are relative to the end of the file. For example,
395      * Reader(taskid, kind, 0, -1) is the entire file and
396      * Reader(taskid, kind, -4197, -1) is the last 4196 bytes.
397      * @param taskid the id of the task to read the log file for
398      * @param kind the kind of log to read
399      * @param start the offset to read from (negative is relative to tail)
400      * @param end the offset to read upto (negative is relative to tail)
401      * @param isCleanup whether the attempt is cleanup attempt or not
402      * @throws IOException
403      */
Reader(TaskAttemptID taskid, LogName kind, long start, long end, boolean isCleanup)404     public Reader(TaskAttemptID taskid, LogName kind,
405                   long start, long end, boolean isCleanup) throws IOException {
406       // find the right log file
407       LogFileDetail fileDetail = getLogFileDetail(taskid, kind, isCleanup);
408       // calculate the start and stop
409       long size = fileDetail.length;
410       if (start < 0) {
411         start += size + 1;
412       }
413       if (end < 0) {
414         end += size + 1;
415       }
416       start = Math.max(0, Math.min(start, size));
417       end = Math.max(0, Math.min(end, size));
418       start += fileDetail.start;
419       end += fileDetail.start;
420       bytesRemaining = end - start;
421       String owner = obtainLogDirOwner(taskid);
422       file = SecureIOUtils.openForRead(new File(fileDetail.location, kind.toString()),
423           owner, null);
424       // skip upto start
425       long pos = 0;
426       while (pos < start) {
427         long result = file.skip(start - pos);
428         if (result < 0) {
429           bytesRemaining = 0;
430           break;
431         }
432         pos += result;
433       }
434     }
435 
436     @Override
read()437     public int read() throws IOException {
438       int result = -1;
439       if (bytesRemaining > 0) {
440         bytesRemaining -= 1;
441         result = file.read();
442       }
443       return result;
444     }
445 
446     @Override
read(byte[] buffer, int offset, int length)447     public int read(byte[] buffer, int offset, int length) throws IOException {
448       length = (int) Math.min(length, bytesRemaining);
449       int bytes = file.read(buffer, offset, length);
450       if (bytes > 0) {
451         bytesRemaining -= bytes;
452       }
453       return bytes;
454     }
455 
456     @Override
available()457     public int available() throws IOException {
458       return (int) Math.min(bytesRemaining, file.available());
459     }
460 
461     @Override
close()462     public void close() throws IOException {
463       file.close();
464     }
465   }
466 
467   private static final String bashCommand = "bash";
468   private static final String tailCommand = "tail";
469 
470   /**
471    * Get the desired maximum length of task's logs.
472    * @param conf the job to look in
473    * @return the number of bytes to cap the log files at
474    */
getTaskLogLength(JobConf conf)475   public static long getTaskLogLength(JobConf conf) {
476    return getTaskLogLimitBytes(conf);
477   }
478 
getTaskLogLimitBytes(Configuration conf)479   public static long getTaskLogLimitBytes(Configuration conf) {
480     return conf.getLong(JobContext.TASK_USERLOG_LIMIT, 0) * 1024;
481   }
482 
483 
484   /**
485    * Wrap a command in a shell to capture stdout and stderr to files.
486    * Setup commands such as setting memory limit can be passed which
487    * will be executed before exec.
488    * If the tailLength is 0, the entire output will be saved.
489    * @param setup The setup commands for the execed process.
490    * @param cmd The command and the arguments that should be run
491    * @param stdoutFilename The filename that stdout should be saved to
492    * @param stderrFilename The filename that stderr should be saved to
493    * @param tailLength The length of the tail to be saved.
494    * @param useSetsid Should setsid be used in the command or not.
495    * @return the modified command that should be run
496    */
captureOutAndError(List<String> setup, List<String> cmd, File stdoutFilename, File stderrFilename, long tailLength, boolean useSetsid )497   public static List<String> captureOutAndError(List<String> setup,
498                                                 List<String> cmd,
499                                                 File stdoutFilename,
500                                                 File stderrFilename,
501                                                 long tailLength,
502                                                 boolean useSetsid
503                                                ) throws IOException {
504     List<String> result = new ArrayList<String>(3);
505     result.add(bashCommand);
506     result.add("-c");
507     String mergedCmd = buildCommandLine(setup, cmd, stdoutFilename,
508                                                     stderrFilename, tailLength,
509                                                     useSetsid);
510     result.add(mergedCmd);
511     return result;
512   }
513 
514   /**
515    * Construct the command line for running the task JVM
516    * @param setup The setup commands for the execed process.
517    * @param cmd The command and the arguments that should be run
518    * @param stdoutFilename The filename that stdout should be saved to
519    * @param stderrFilename The filename that stderr should be saved to
520    * @param tailLength The length of the tail to be saved.
521    * @return the command line as a String
522    * @throws IOException
523    */
buildCommandLine(List<String> setup, List<String> cmd, File stdoutFilename, File stderrFilename, long tailLength, boolean useSetsid)524   static String buildCommandLine(List<String> setup, List<String> cmd,
525                                       File stdoutFilename,
526                                       File stderrFilename,
527                                       long tailLength,
528                                       boolean useSetsid)
529                                 throws IOException {
530 
531     String stdout = FileUtil.makeShellPath(stdoutFilename);
532     String stderr = FileUtil.makeShellPath(stderrFilename);
533     StringBuffer mergedCmd = new StringBuffer();
534 
535     // Export the pid of taskJvm to env variable JVM_PID.
536     // Currently pid is not used on Windows
537     if (!Shell.WINDOWS) {
538       mergedCmd.append(" export JVM_PID=`echo $$` ; ");
539     }
540 
541     if (setup != null && setup.size() > 0) {
542       mergedCmd.append(addCommand(setup, false));
543       mergedCmd.append(";");
544     }
545     if (tailLength > 0) {
546       mergedCmd.append("(");
547     } else if(ProcessTree.isSetsidAvailable && useSetsid &&
548         !Shell.WINDOWS) {
549       mergedCmd.append("exec setsid ");
550     } else {
551       mergedCmd.append("exec ");
552     }
553     mergedCmd.append(addCommand(cmd, true));
554     mergedCmd.append(" < /dev/null ");
555     if (tailLength > 0) {
556       mergedCmd.append(" | ");
557       mergedCmd.append(tailCommand);
558       mergedCmd.append(" -c ");
559       mergedCmd.append(tailLength);
560       mergedCmd.append(" >> ");
561       mergedCmd.append(stdout);
562       mergedCmd.append(" ; exit $PIPESTATUS ) 2>&1 | ");
563       mergedCmd.append(tailCommand);
564       mergedCmd.append(" -c ");
565       mergedCmd.append(tailLength);
566       mergedCmd.append(" >> ");
567       mergedCmd.append(stderr);
568       mergedCmd.append(" ; exit $PIPESTATUS");
569     } else {
570       mergedCmd.append(" 1>> ");
571       mergedCmd.append(stdout);
572       mergedCmd.append(" 2>> ");
573       mergedCmd.append(stderr);
574     }
575     return mergedCmd.toString();
576   }
577 
578   /**
579    * Construct the command line for running the debug script
580    * @param cmd The command and the arguments that should be run
581    * @param stdoutFilename The filename that stdout should be saved to
582    * @param stderrFilename The filename that stderr should be saved to
583    * @param tailLength The length of the tail to be saved.
584    * @return the command line as a String
585    * @throws IOException
586    */
buildDebugScriptCommandLine(List<String> cmd, String debugout)587   static String buildDebugScriptCommandLine(List<String> cmd, String debugout)
588   throws IOException {
589     StringBuilder mergedCmd = new StringBuilder();
590     mergedCmd.append("exec ");
591     boolean isExecutable = true;
592     for(String s: cmd) {
593       if (isExecutable) {
594         // the executable name needs to be expressed as a shell path for the
595         // shell to find it.
596         mergedCmd.append(FileUtil.makeShellPath(new File(s)));
597         isExecutable = false;
598       } else {
599         mergedCmd.append(s);
600       }
601       mergedCmd.append(" ");
602     }
603     mergedCmd.append(" < /dev/null ");
604     mergedCmd.append(" >");
605     mergedCmd.append(debugout);
606     mergedCmd.append(" 2>&1 ");
607     return mergedCmd.toString();
608   }
609   /**
610    * Add quotes to each of the command strings and
611    * return as a single string
612    * @param cmd The command to be quoted
613    * @param isExecutable makes shell path if the first
614    * argument is executable
615    * @return returns The quoted string.
616    * @throws IOException
617    */
addCommand(List<String> cmd, boolean isExecutable)618   public static String addCommand(List<String> cmd, boolean isExecutable)
619   throws IOException {
620     StringBuffer command = new StringBuffer();
621     for(String s: cmd) {
622     	command.append('\'');
623       if (isExecutable) {
624         // the executable name needs to be expressed as a shell path for the
625         // shell to find it.
626     	  command.append(FileUtil.makeShellPath(new File(s)));
627         isExecutable = false;
628       } else {
629     	  command.append(s);
630       }
631       command.append('\'');
632       command.append(" ");
633     }
634     return command.toString();
635   }
636 
637 
638   /**
639    * Method to return the location of user log directory.
640    *
641    * @return base log directory
642    */
getUserLogDir()643   static File getUserLogDir() {
644     if (!LOG_DIR.exists()) {
645       boolean b = LOG_DIR.mkdirs();
646       if (!b) {
647         LOG.debug("mkdirs failed. Ignoring.");
648       }
649     }
650     return LOG_DIR;
651   }
652 
653   /**
654    * Get the user log directory for the job jobid.
655    *
656    * @param jobid
657    * @return user log directory for the job
658    */
getJobDir(JobID jobid)659   public static File getJobDir(JobID jobid) {
660     return new File(getUserLogDir(), jobid.toString());
661   }
662 
663 } // TaskLog
664