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
019package org.apache.oozie.service;
020
021import org.apache.commons.logging.LogFactory;
022import org.apache.log4j.LogManager;
023import org.apache.log4j.PropertyConfigurator;
024import org.apache.oozie.util.XLogFilter;
025import org.apache.oozie.util.Instrumentable;
026import org.apache.oozie.util.Instrumentation;
027import org.apache.oozie.util.XLog;
028import org.apache.oozie.util.XConfiguration;
029import org.apache.oozie.BuildInfo;
030import org.apache.oozie.ErrorCode;
031import org.apache.hadoop.conf.Configuration;
032
033import java.io.File;
034import java.io.FileInputStream;
035import java.io.IOException;
036import java.io.InputStream;
037import java.net.URL;
038import java.util.Properties;
039import java.util.Map;
040import java.util.regex.Pattern;
041
042/**
043 * Built-in service that initializes and manages Logging via Log4j.
044 * <p/>
045 * Oozie Lo4gj default configuration file is <code>oozie-log4j.properties</code>.
046 * <p/>
047 * The file name can be changed by setting the Java System property <code>oozie.log4j.file</code>.
048 * <p/>
049 * The Log4j configuration files must be a properties file.
050 * <p/>
051 * The Log4j configuration file is first looked in the Oozie configuration directory see {@link ConfigurationService}.
052 * If the file is not found there, it is looked in the classpath.
053 * <p/>
054 * If the Log4j configuration file is loaded from Oozie configuration directory, automatic reloading is enabled.
055 * <p/>
056 * If the Log4j configuration file is loaded from the classpath, automatic reloading is disabled.
057 * <p/>
058 * the automatic reloading interval is defined by the Java System property <code>oozie.log4j.reload</code>. The default
059 * value is 10 seconds.
060 * <p>
061 * <p>
062 * Unlike most of the other Services, XLogService isn't easily overridable because Services depends on XLogService being available
063 */
064public class XLogService implements Service, Instrumentable {
065    private static final String INSTRUMENTATION_GROUP = "logging";
066
067    /**
068     * System property that indicates the logs directory.
069     */
070    public static final String OOZIE_LOG_DIR = "oozie.log.dir";
071
072    /**
073     * System property that indicates the log4j configuration file to load.
074     */
075    public static final String LOG4J_FILE = "oozie.log4j.file";
076
077    /**
078     * System property that indicates the reload interval of the configuration file.
079     */
080    public static final String LOG4J_RELOAD = "oozie.log4j.reload";
081
082    /**
083     * Default value for the log4j configuration file if {@link #LOG4J_FILE} is not set.
084     */
085    public static final String DEFAULT_LOG4J_PROPERTIES = "oozie-log4j.properties";
086
087    /**
088     * Default value for the reload interval if {@link #LOG4J_RELOAD} is not set.
089     */
090    public static final String DEFAULT_RELOAD_INTERVAL = "10";
091
092    private XLog log;
093    private long interval;
094    private boolean fromClasspath;
095    private String log4jFileName;
096    private boolean logOverWS = true;
097
098    private static final String STARTUP_MESSAGE = "{E}"
099            + " ******************************************************************************* {E}"
100            + "  STARTUP MSG: Oozie BUILD_VERSION [{0}] compiled by [{1}] on [{2}]{E}"
101            + "  STARTUP MSG:       revision [{3}]@[{4}]{E}"
102            + "*******************************************************************************";
103
104    private String oozieLogPath;
105    private String oozieLogName;
106    private int oozieLogRotation = -1;
107
108    public XLogService() {
109    }
110    
111    public String getOozieLogPath() {
112        return oozieLogPath;
113    }
114    
115    public String getOozieLogName() {
116        return oozieLogName;
117    }
118
119    /**
120     * Initialize the log service.
121     *
122     * @param services services instance.
123     * @throws ServiceException thrown if the log service could not be initialized.
124     */
125    public void init(Services services) throws ServiceException {
126        String oozieHome = Services.getOozieHome();
127        String oozieLogs = System.getProperty(OOZIE_LOG_DIR, oozieHome + "/logs");
128        System.setProperty(OOZIE_LOG_DIR, oozieLogs);
129
130        try {
131            LogManager.resetConfiguration();
132            log4jFileName = System.getProperty(LOG4J_FILE, DEFAULT_LOG4J_PROPERTIES);
133            if (log4jFileName.contains("/")) {
134                throw new ServiceException(ErrorCode.E0011, log4jFileName);
135            }
136            if (!log4jFileName.endsWith(".properties")) {
137                throw new ServiceException(ErrorCode.E0012, log4jFileName);
138            }
139            String configPath = ConfigurationService.getConfigurationDirectory();
140            File log4jFile = new File(configPath, log4jFileName);
141            if (log4jFile.exists()) {
142                fromClasspath = false;
143            }
144            else {
145                ClassLoader cl = Thread.currentThread().getContextClassLoader();
146                URL log4jUrl = cl.getResource(log4jFileName);
147                if (log4jUrl == null) {
148                    throw new ServiceException(ErrorCode.E0013, log4jFileName, configPath);
149                }
150                fromClasspath = true;
151            }
152
153            if (fromClasspath) {
154                ClassLoader cl = Thread.currentThread().getContextClassLoader();
155                URL log4jUrl = cl.getResource(log4jFileName);
156                PropertyConfigurator.configure(log4jUrl);
157            }
158            else {
159                interval = Long.parseLong(System.getProperty(LOG4J_RELOAD, DEFAULT_RELOAD_INTERVAL));
160                PropertyConfigurator.configureAndWatch(log4jFile.toString(), interval * 1000);
161            }
162
163            log = new XLog(LogFactory.getLog(getClass()));
164
165            log.info(XLog.OPS, STARTUP_MESSAGE, BuildInfo.getBuildInfo().getProperty(BuildInfo.BUILD_VERSION),
166                    BuildInfo.getBuildInfo().getProperty(BuildInfo.BUILD_USER_NAME), BuildInfo.getBuildInfo()
167                            .getProperty(BuildInfo.BUILD_TIME), BuildInfo.getBuildInfo().getProperty(
168                            BuildInfo.BUILD_VC_REVISION), BuildInfo.getBuildInfo().getProperty(BuildInfo.BUILD_VC_URL));
169
170            String from = (fromClasspath) ? "CLASSPATH" : configPath;
171            String reload = (fromClasspath) ? "disabled" : Long.toString(interval) + " sec";
172            log.info("Log4j configuration file [{0}]", log4jFileName);
173            log.info("Log4j configuration file loaded from [{0}]", from);
174            log.info("Log4j reload interval [{0}]", reload);
175
176            XLog.Info.reset();
177            XLog.Info.defineParameter(USER);
178            XLog.Info.defineParameter(GROUP);
179            XLogFilter.reset();
180            XLogFilter.defineParameter(USER);
181            XLogFilter.defineParameter(GROUP);
182
183            // Getting configuration for oozie log via WS
184            ClassLoader cl = Thread.currentThread().getContextClassLoader();
185            InputStream is = (fromClasspath) ? cl.getResourceAsStream(log4jFileName) : new FileInputStream(log4jFile);
186            extractInfoForLogWebService(is);
187        }
188        catch (IOException ex) {
189            throw new ServiceException(ErrorCode.E0010, ex.getMessage(), ex);
190        }
191    }
192
193    private void extractInfoForLogWebService(InputStream is) throws IOException {
194        logOverWS = true;
195        Properties props = new Properties();
196        props.load(is);
197
198        XConfiguration conf = new XConfiguration();
199        conf.setRestrictSystemProperties(false);
200        for (Map.Entry entry : props.entrySet()) {
201            conf.set((String) entry.getKey(), (String) entry.getValue());
202        }
203        String logFile = conf.get("log4j.appender.oozie.File");
204        if (logFile == null) {
205            log.warn("Oozie WS log will be disabled, missing property 'log4j.appender.oozie.File' for 'oozie' "
206                    + "appender");
207            logOverWS = false;
208        }
209        else {
210            logFile = logFile.trim();
211            int i = logFile.lastIndexOf("/");
212            if (i == -1) {
213                log.warn("Oozie WS log will be disabled, log file is not an absolute path [{0}] for 'oozie' appender",
214                        logFile);
215                logOverWS = false;
216            }
217            else {
218                String appenderClass = conf.get("log4j.appender.oozie");
219                if (appenderClass == null) {
220                    log.warn("Oozie WS log will be disabled, missing property [log4j.appender.oozie]");
221                    logOverWS = false;
222                }
223                else if (appenderClass.equals("org.apache.log4j.DailyRollingFileAppender")) {
224                    String pattern = conf.get("log4j.appender.oozie.DatePattern");
225                    if (pattern == null) {
226                        log.warn("Oozie WS log will be disabled, missing property [log4j.appender.oozie.DatePattern]");
227                        logOverWS = false;
228                    }
229                    else {
230                        pattern = pattern.trim();
231                        if (pattern.endsWith("HH")) {
232                            oozieLogRotation = 60 * 60;
233                        }
234                        else if (pattern.endsWith("dd")) {
235                                oozieLogRotation = 60 * 60 * 24;
236                        }
237                        else {
238                            log.warn("Oozie WS log will be disabled, DatePattern [{0}] should end with 'HH' or 'dd'",
239                                    pattern);
240                            logOverWS = false;
241                        }
242                        if (oozieLogRotation > 0) {
243                            oozieLogPath = logFile.substring(0, i);
244                            oozieLogName = logFile.substring(i + 1);
245                        }
246                    }
247                }
248                else if (appenderClass.equals("org.apache.log4j.rolling.RollingFileAppender")) {
249                    String pattern = conf.get("log4j.appender.oozie.RollingPolicy.FileNamePattern");
250                    if (pattern == null) {
251                        log.warn("Oozie WS log will be disabled, missing property "
252                                + "[log4j.appender.oozie.RollingPolicy.FileNamePattern]");
253                        logOverWS = false;
254                    }
255                    else {
256                        pattern = pattern.trim();
257                        if (pattern.matches(Pattern.quote(logFile) + ".*-%d\\{yyyy-MM-dd-HH\\}(\\.gz)?")) {
258                            oozieLogRotation = 60 * 60;
259                        }
260                        else {
261                            log.warn("Oozie WS log will be disabled, RollingPolicy.FileNamePattern [{0}] should end with " 
262                                    + "'-%d{yyyy-MM-dd-HH}' or '-%d{yyyy-MM-dd-HH}.gz' and also start with the value of "
263                                    + "log4j.appender.oozie.File [{1}]", pattern, logFile);
264                            logOverWS = false;
265                        }
266                        if (oozieLogRotation > 0) {
267                            oozieLogPath = logFile.substring(0, i);
268                            oozieLogName = logFile.substring(i + 1);
269                        }
270                    }
271                }
272                else {
273                    log.warn("Oozie WS log will be disabled, log4j.appender.oozie [" + appenderClass + "] should be "
274                            + "either org.apache.log4j.DailyRollingFileAppender or org.apache.log4j.rolling.RollingFileAppender "
275                            + "to enable it");
276                    logOverWS = false;
277                }
278            }
279        }
280    }
281
282    /**
283     * Destroy the log service.
284     */
285    public void destroy() {
286        LogManager.shutdown();
287        XLog.Info.reset();
288        XLogFilter.reset();
289    }
290
291    /**
292     * Group log info constant.
293     */
294    public static final String USER = "USER";
295
296    /**
297     * Group log info constant.
298     */
299    public static final String GROUP = "GROUP";
300
301    /**
302     * Return the public interface for log service.
303     *
304     * @return {@link XLogService}.
305     */
306    public Class<? extends Service> getInterface() {
307        return XLogService.class;
308    }
309
310    /**
311     * Instruments the log service.
312     * <p/>
313     * It sets instrumentation variables indicating the config file, reload interval and if loaded from the classpath.
314     *
315     * @param instr instrumentation to use.
316     */
317    public void instrument(Instrumentation instr) {
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    boolean getLogOverWS() {
341        return logOverWS;
342    }
343
344    int getOozieLogRotation() {
345        return oozieLogRotation;
346    }
347
348    String getLog4jProperties() {
349        return log4jFileName;
350    }
351
352    boolean getFromClasspath() {
353        return fromClasspath;
354    }
355
356}