001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     * 
010     *      http://www.apache.org/licenses/LICENSE-2.0
011     * 
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    package org.apache.oozie.service;
019    
020    import org.apache.commons.logging.LogFactory;
021    import org.apache.log4j.LogManager;
022    import org.apache.log4j.PropertyConfigurator;
023    import org.apache.oozie.util.Instrumentable;
024    import org.apache.oozie.util.Instrumentation;
025    import org.apache.oozie.util.XLog;
026    import org.apache.oozie.util.XLogStreamer;
027    import org.apache.oozie.util.XConfiguration;
028    import org.apache.oozie.BuildInfo;
029    import org.apache.oozie.ErrorCode;
030    import org.apache.hadoop.conf.Configuration;
031    
032    import java.io.File;
033    import java.io.FileInputStream;
034    import java.io.IOException;
035    import java.io.InputStream;
036    import java.io.Writer;
037    import java.net.URL;
038    import java.util.Properties;
039    import java.util.Map;
040    import java.util.Date;
041    import java.util.regex.Pattern;
042    
043    /**
044     * Built-in service that initializes and manages Logging via Log4j.
045     * <p/>
046     * Oozie Lo4gj default configuration file is <code>oozie-log4j.properties</code>.
047     * <p/>
048     * The file name can be changed by setting the Java System property <code>oozie.log4j.file</code>.
049     * <p/>
050     * The Log4j configuration files must be a properties file.
051     * <p/>
052     * The Log4j configuration file is first looked in the Oozie configuration directory see {@link ConfigurationService}.
053     * If the file is not found there, it is looked in the classpath.
054     * <p/>
055     * If the Log4j configuration file is loaded from Oozie configuration directory, automatic reloading is enabled.
056     * <p/>
057     * If the Log4j configuration file is loaded from the classpath, automatic reloading is disabled.
058     * <p/>
059     * the automatic reloading interval is defined by the Java System property <code>oozie.log4j.reload</code>. The default
060     * value is 10 seconds.
061     */
062    public class XLogService implements Service, Instrumentable {
063        private static final String INSTRUMENTATION_GROUP = "logging";
064    
065        /**
066         * System property that indicates the logs directory.
067         */
068        public static final String OOZIE_LOG_DIR = "oozie.log.dir";
069    
070        /**
071         * System property that indicates the log4j configuration file to load.
072         */
073        public static final String LOG4J_FILE = "oozie.log4j.file";
074    
075        /**
076         * System property that indicates the reload interval of the configuration file.
077         */
078        public static final String LOG4J_RELOAD = "oozie.log4j.reload";
079    
080        /**
081         * Default value for the log4j configuration file if {@link #LOG4J_FILE} is not set.
082         */
083        public static final String DEFAULT_LOG4J_PROPERTIES = "oozie-log4j.properties";
084    
085        /**
086         * Default value for the reload interval if {@link #LOG4J_RELOAD} is not set.
087         */
088        public static final String DEFAULT_RELOAD_INTERVAL = "10";
089    
090        private XLog log;
091        private long interval;
092        private boolean fromClasspath;
093        private String log4jFileName;
094        private boolean logOverWS = true;
095    
096        private static final String STARTUP_MESSAGE = "{E}"
097                + " ******************************************************************************* {E}"
098                + "  STARTUP MSG: Oozie BUILD_VERSION [{0}] compiled by [{1}] on [{2}]{E}"
099                + "  STARTUP MSG:       revision [{3}]@[{4}]{E}"
100                + "*******************************************************************************";
101    
102        private String oozieLogPath;
103        private String oozieLogName;
104        private int oozieLogRotation = -1;
105    
106        public XLogService() {
107        }
108        
109        public String getOozieLogPath() {
110            return oozieLogPath;
111        }
112        
113        public String getOozieLogName() {
114            return oozieLogName;
115        }
116    
117        /**
118         * Initialize the log service.
119         *
120         * @param services services instance.
121         * @throws ServiceException thrown if the log service could not be initialized.
122         */
123        public void init(Services services) throws ServiceException {
124            String oozieHome = Services.getOozieHome();
125            String oozieLogs = System.getProperty(OOZIE_LOG_DIR, oozieHome + "/logs");
126            System.setProperty(OOZIE_LOG_DIR, oozieLogs);
127            try {
128                LogManager.resetConfiguration();
129                log4jFileName = System.getProperty(LOG4J_FILE, DEFAULT_LOG4J_PROPERTIES);
130                if (log4jFileName.contains("/")) {
131                    throw new ServiceException(ErrorCode.E0011, log4jFileName);
132                }
133                if (!log4jFileName.endsWith(".properties")) {
134                    throw new ServiceException(ErrorCode.E0012, log4jFileName);
135                }
136                String configPath = ConfigurationService.getConfigurationDirectory();
137                File log4jFile = new File(configPath, log4jFileName);
138                if (log4jFile.exists()) {
139                    fromClasspath = false;
140                }
141                else {
142                    ClassLoader cl = Thread.currentThread().getContextClassLoader();
143                    URL log4jUrl = cl.getResource(log4jFileName);
144                    if (log4jUrl == null) {
145                        throw new ServiceException(ErrorCode.E0013, log4jFileName, configPath);
146                    }
147                    fromClasspath = true;
148                }
149    
150                if (fromClasspath) {
151                    ClassLoader cl = Thread.currentThread().getContextClassLoader();
152                    URL log4jUrl = cl.getResource(log4jFileName);
153                    PropertyConfigurator.configure(log4jUrl);
154                }
155                else {
156                    interval = Long.parseLong(System.getProperty(LOG4J_RELOAD, DEFAULT_RELOAD_INTERVAL));
157                    PropertyConfigurator.configureAndWatch(log4jFile.toString(), interval * 1000);
158                }
159    
160                log = new XLog(LogFactory.getLog(getClass()));
161    
162                log.info(XLog.OPS, STARTUP_MESSAGE, BuildInfo.getBuildInfo().getProperty(BuildInfo.BUILD_VERSION),
163                        BuildInfo.getBuildInfo().getProperty(BuildInfo.BUILD_USER_NAME), BuildInfo.getBuildInfo()
164                                .getProperty(BuildInfo.BUILD_TIME), BuildInfo.getBuildInfo().getProperty(
165                                BuildInfo.BUILD_VC_REVISION), BuildInfo.getBuildInfo().getProperty(BuildInfo.BUILD_VC_URL));
166    
167                String from = (fromClasspath) ? "CLASSPATH" : configPath;
168                String reload = (fromClasspath) ? "disabled" : Long.toString(interval) + " sec";
169                log.info("Log4j configuration file [{0}]", log4jFileName);
170                log.info("Log4j configuration file loaded from [{0}]", from);
171                log.info("Log4j reload interval [{0}]", reload);
172    
173                XLog.Info.reset();
174                XLog.Info.defineParameter(USER);
175                XLog.Info.defineParameter(GROUP);
176                XLogStreamer.Filter.reset();
177                XLogStreamer.Filter.defineParameter(USER);
178                XLogStreamer.Filter.defineParameter(GROUP);
179    
180                // Getting configuration for oozie log via WS
181                ClassLoader cl = Thread.currentThread().getContextClassLoader();
182                InputStream is = (fromClasspath) ? cl.getResourceAsStream(log4jFileName) : new FileInputStream(log4jFile);
183                extractInfoForLogWebService(is);
184            }
185            catch (IOException ex) {
186                throw new ServiceException(ErrorCode.E0010, ex.getMessage(), ex);
187            }
188        }
189    
190        private void extractInfoForLogWebService(InputStream is) throws IOException {
191            Properties props = new Properties();
192            props.load(is);
193    
194            Configuration conf = new XConfiguration();
195            for (Map.Entry entry : props.entrySet()) {
196                conf.set((String) entry.getKey(), (String) entry.getValue());
197            }
198            String logFile = conf.get("log4j.appender.oozie.File");
199            if (logFile == null) {
200                log.warn("Oozie WS log will be disabled, missing property 'log4j.appender.oozie.File' for 'oozie' "
201                        + "appender");
202                logOverWS = false;
203            }
204            else {
205                logFile = logFile.trim();
206                int i = logFile.lastIndexOf("/");
207                if (i == -1) {
208                    log.warn("Oozie WS log will be disabled, log file is not an absolute path [{0}] for 'oozie' appender",
209                            logFile);
210                    logOverWS = false;
211                }
212                else {
213                    String appenderClass = conf.get("log4j.appender.oozie");
214                    if (appenderClass == null) {
215                        log.warn("Oozie WS log will be disabled, missing property [log4j.appender.oozie]");
216                        logOverWS = false;
217                    }
218                    else if (appenderClass.equals("org.apache.log4j.DailyRollingFileAppender")) {
219                        String pattern = conf.get("log4j.appender.oozie.DatePattern");
220                        if (pattern == null) {
221                            log.warn("Oozie WS log will be disabled, missing property [log4j.appender.oozie.DatePattern]");
222                            logOverWS = false;
223                        }
224                        else {
225                            pattern = pattern.trim();
226                            if (pattern.endsWith("HH")) {
227                                oozieLogRotation = 60 * 60;
228                            }
229                            else if (pattern.endsWith("dd")) {
230                                    oozieLogRotation = 60 * 60 * 24;
231                            }
232                            else {
233                                log.warn("Oozie WS log will be disabled, DatePattern [{0}] should end with 'HH' or 'dd'",
234                                        pattern);
235                                logOverWS = false;
236                            }
237                            if (oozieLogRotation > 0) {
238                                oozieLogPath = logFile.substring(0, i);
239                                oozieLogName = logFile.substring(i + 1);
240                            }
241                        }
242                    }
243                    else if (appenderClass.equals("org.apache.log4j.rolling.RollingFileAppender")) {
244                        String pattern = conf.get("log4j.appender.oozie.RollingPolicy.FileNamePattern");
245                        if (pattern == null) {
246                            log.warn("Oozie WS log will be disabled, missing property "
247                                    + "[log4j.appender.oozie.RollingPolicy.FileNamePattern]");
248                            logOverWS = false;
249                        }
250                        else {
251                            pattern = pattern.trim();
252                            if (pattern.matches(Pattern.quote(logFile) + ".*-%d\\{yyyy-MM-dd-HH\\}(\\.gz)?")) {
253                                oozieLogRotation = 60 * 60;
254                            }
255                            else {
256                                log.warn("Oozie WS log will be disabled, RollingPolicy.FileNamePattern [{0}] should end with " 
257                                        + "'-%d{yyyy-MM-dd-HH}' or '-%d{yyyy-MM-dd-HH}.gz' and also start with the value of "
258                                        + "log4j.appender.oozie.File [{1}]", pattern, logFile);
259                                logOverWS = false;
260                            }
261                            if (oozieLogRotation > 0) {
262                                oozieLogPath = logFile.substring(0, i);
263                                oozieLogName = logFile.substring(i + 1);
264                            }
265                        }
266                    }
267                    else {
268                        log.warn("Oozie WS log will be disabled, log4j.appender.oozie [" + appenderClass + "] should be "
269                                + "either org.apache.log4j.DailyRollingFileAppender or org.apache.log4j.rolling.RollingFileAppender "
270                                + "to enable it");
271                        logOverWS = false;
272                    }
273                }
274            }
275        }
276    
277        /**
278         * Destroy the log service.
279         */
280        public void destroy() {
281            LogManager.shutdown();
282            XLog.Info.reset();
283            XLogStreamer.Filter.reset();
284        }
285    
286        /**
287         * Group log info constant.
288         */
289        public static final String USER = "USER";
290    
291        /**
292         * Group log info constant.
293         */
294        public static final String GROUP = "GROUP";
295    
296        /**
297         * Return the public interface for log service.
298         *
299         * @return {@link XLogService}.
300         */
301        public Class<? extends Service> getInterface() {
302            return XLogService.class;
303        }
304    
305        /**
306         * Instruments the log service.
307         * <p/>
308         * It sets instrumentation variables indicating the config file, reload interval and if loaded from the classpath.
309         *
310         * @param instr instrumentation to use.
311         */
312        public void instrument(Instrumentation instr) {
313            instr.addVariable("oozie", "version", new Instrumentation.Variable<String>() {
314                public String getValue() {
315                    return BuildInfo.getBuildInfo().getProperty(BuildInfo.BUILD_VERSION);
316                }
317            });
318            instr.addVariable(INSTRUMENTATION_GROUP, "config.file", new Instrumentation.Variable<String>() {
319                public String getValue() {
320                    return log4jFileName;
321                }
322            });
323            instr.addVariable(INSTRUMENTATION_GROUP, "reload.interval", new Instrumentation.Variable<Long>() {
324                public Long getValue() {
325                    return interval;
326                }
327            });
328            instr.addVariable(INSTRUMENTATION_GROUP, "from.classpath", new Instrumentation.Variable<Boolean>() {
329                public Boolean getValue() {
330                    return fromClasspath;
331                }
332            });
333            instr.addVariable(INSTRUMENTATION_GROUP, "log.over.web-service", new Instrumentation.Variable<Boolean>() {
334                public Boolean getValue() {
335                    return logOverWS;
336                }
337            });
338        }
339    
340        /**
341         * Stream the log of a job.
342         *
343         * @param filter log streamer filter.
344         * @param startTime start time for log events to filter.
345         * @param endTime end time for log events to filter.
346         * @param writer writer to stream the log to.
347         * @throws IOException thrown if the log cannot be streamed.
348         */
349        public void streamLog(XLogStreamer.Filter filter, Date startTime, Date endTime, Writer writer) throws IOException {
350            if (logOverWS) {
351                new XLogStreamer(filter, writer, oozieLogPath, oozieLogName, oozieLogRotation)
352                        .streamLog(startTime, endTime);
353            }
354            else {
355                writer.write("Log streaming disabled!!");
356            }
357    
358        }
359    
360        String getLog4jProperties() {
361            return log4jFileName;
362        }
363    
364        boolean getFromClasspath() {
365            return fromClasspath;
366        }
367    
368    }