// Copyright 2011 Google Inc.
//
// Licensed 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 com.google.enterprise.connector.servlet;
import com.google.common.base.Strings;
import com.google.enterprise.connector.common.PropertiesException;
import com.google.enterprise.connector.logging.NDC;
import com.google.enterprise.connector.manager.ConnectorManagerException;
import com.google.enterprise.connector.manager.Context;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* <p>Admin servlet to set and fetch the logging {@link Level} for the
* Connector Manager's Connector logs or Feed logs.
* <p>
* This servlet allows the user to alter the level of logging detail
* dynamically, without restarting the Connector Manager. More detailed
* logs are especially useful when trying to troubleshoot connector problems.
* <p>
* Connector logs contain the status of the Connector Manager and all
* Connector instances as they perform their various tasks.
* Feed logs identify each document, and it associated meta-data, as
* they are feed to the GSA.
* <p>
* The recognized logging levels in increasing levels of detail are:
* <ul><li>{@code SEVERE} - Catastrophic failure; the Connector cannot continue
* running.</li>
* <li>{@code WARNING} - Unexpected exceptional conditions, the product may
* not function properly.</li>
* <li>{@code INFO} - Informational message. This is the default connector
* logging level upon installation.</li>
* <li>{@code CONFIG} - Configuration details and higher product functions.</li>
* <li>{@code FINE} - Generally information related to batches of documents.</li>
* <li>{@code FINER} - Generally information related individual documents.</li>
* <li>{@code FINEST} - Generally information related to document meta-data,
* processing detail that is exceptionally verbose.</li>
* </ul>
* <p>In addition to the above logging levels, the following psuedo-levels are
* recognized:
* <ul><li>{@code OFF} - Logging is turned off. This is the default feed
* logging level upon installation.</li>
* <li>{@code ALL} - Enable most detailed logging (alias of {@code FINEST}).</li>
* </ul>
* {@code ALL} and {@code OFF} are more conveniently used with feed logging,
* which is currently an "all or nothing" style implementation.
* <p>
* After altering the desired logging level, allow the product to run for
* the prescribed time (anywhere from several minutes to days). Then the
* {@link com.google.enterprise.connector.servlet.GetConnectorLogs GetConnectorLogs}
* servlet may subsequently be used to fetch the accumulated log files for
* analysis.
*
* <p><b>Usage:</b>
* <br>To fetch the current connector logging level:
* <br><pre> http://[cm_host_addr]/connector-manager/getConnectorLogLevel</pre>
* </p>
* <p>To set the connector logging level:
* <br><pre> http://[cm_host_addr]/connector-manager/setConnectorLogLevel?level=[level]</pre>
* <br>where [level] is one of the previously defined logging levels. Setting
* the connector logging level to {@code INFO} restores it to the default
* configuration. Setting the connector logging level to {@code OFF} is
* not recommended.
* <p>For instance:
* <br><pre> http://[cm_host_addr]/connector-manager/setConnectorLogLevel?level=ALL</pre>
*
* <p><br>To fetch the current feed logging level:
* <br><pre> http://[cm_host_addr]/connector-manager/getFeedLogLevel</pre></p>
*
* <p>To set the feed logging level:
* <br><pre> http://[cm_host_addr]/connector-manager/setFeedLogLevel?level=[level]</pre>
* <br>where [level] is one of the previously defined logging levels. For
* feed logs, the recommended levels are {@code OFF} and {@code ALL}.
* <p>For instance:
* <br><pre> http://[cm_host_addr]/connector-manager/setFeedLogLevel?level=ALL</pre>
*/
public class LogLevel extends HttpServlet {
private static final Logger LOGGER =
Logger.getLogger(LogLevel.class.getName());
/**
* Specialized {@code doTrace} method that constructs an XML representation
* of the given request and returns it as the response.
*/
@Override
protected void doTrace(HttpServletRequest req, HttpServletResponse res)
throws IOException {
ServletDump.dumpServletRequest(req, res);
}
/**
* Sets Logging levels for connectors and feeds.
*
* @param req
* @param res
* @throws IOException
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res)
throws IOException {
doPost(req, res);
}
/**
* Sets Logging levels for connectors and feeds.
*
* @param req
* @param res
* @throws IOException
*/
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse res)
throws IOException {
// Make sure this requester is OK
if (!RemoteAddressFilter.getInstance()
.allowed(RemoteAddressFilter.Access.RED, req.getRemoteAddr())) {
res.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
res.setContentType(ServletUtil.MIMETYPE_XML);
PrintWriter out = res.getWriter();
NDC.pushAppend("Support");
try {
// Are we setting the Level for Connector logs or Feed logs?
LogLevelHandler handler;
if (req.getServletPath().indexOf("Feed") > 0) {
handler = new FeedLogLevelHandler();
} else {
handler = new ConnectorLogLevelHandler();
}
handleDoPost(req.getServletPath(), handler, req.getParameter("level"),
out);
} finally {
out.close();
NDC.pop();
}
}
/**
* Sets Logging levels for connectors and feeds.
*
* @param servletName the name of the servlet
* @param handler a LogLevelHandler
* @param logLevel new logging Level
* @param out a PrintWriter
* @throws IOException
*/
// TODO: This extracted method is now testable, so write some tests.
private void handleDoPost(String servletName, LogLevelHandler handler,
String logLevel, PrintWriter out) throws IOException {
try {
// Set the logging level, if one was specified.
if (!Strings.isNullOrEmpty(logLevel)) {
Level level = getLevelByName(logLevel.toUpperCase());
LOGGER.config("Setting " + handler.getName()
+ " Logging level to " + level.getName());
handler.getLogger().setLevel(level);
handler.persistLevel(level);
}
// Return Status of the current logging level for the handler.
ServletUtil.writeRootTag(out, false);
ServletUtil.writeMessageCode(out, new ConnectorMessageCode());
String currentLevel = getLogLevel(handler.getLogger()).getName();
ServletUtil.writeXMLElement(out, 1, ServletUtil.XMLTAG_LEVEL,
currentLevel);
ServletUtil.writeXMLElement(out, 1, ServletUtil.XMLTAG_INFO,
handler.getName() + " Logging level is " + currentLevel);
ServletUtil.writeRootTag(out, true);
} catch (ConnectorManagerException e) {
LOGGER.log(Level.WARNING, e.getMessage(), e);
// TODO: These should really be new ConnectorMessageCodes
ServletUtil.writeResponse(out, new ConnectorMessageCode(
ConnectorMessageCode.EXCEPTION_HTTP_SERVLET,
servletName + " - " + e.getMessage()));
}
}
/**
* Returns the current logging {@link Level} for the specified
* {@link Logger}. If {@code logger} has no explicitly configured
* logging level, the {@code Level} of the nearest configured ancester
* {@code Logger} is returned.
*
* @param logger a {@link Logger}
* @return the current logging {@link Level} for {@code logger}
*/
private Level getLogLevel(Logger logger) {
while (logger != null) {
Level level = logger.getLevel();
if (level != null) {
return level;
}
logger = logger.getParent();
}
return Level.OFF;
}
/**
* Returns the logging {@link Level} for the given name.
*
* @param name the name of a logging {@link Level}
* @return the logging {@link Level} identified by {@code name}
* @throws ConnectorManagerException if {@code name} is not a valid
* {@link Level}
*/
private Level getLevelByName(String name) throws ConnectorManagerException {
try {
return Level.parse(name);
} catch (IllegalArgumentException e) {
throw new ConnectorManagerException("Unknown logging level: " + name, e);
}
}
/**
* Abstract access to either the Connector Logs or the Feed Logs.
*/
private static interface LogLevelHandler {
/**
* Returns a descriptive name of the {@code LogLevelHandler}.
*/
public String getName();
/**
* Returns the {@link Logger} managed by this {@code LogLevelHandler}.
*/
public Logger getLogger() throws ConnectorManagerException;
/**
* Persists the specified {@link Level} for this {@code LogLevelHandler}.
*
* @param level the logging level to save.
*/
public void persistLevel(Level level) throws ConnectorManagerException;
}
/**
* A LogLevelHandler for connector logs, as configured in logging.properties.
*/
private static class ConnectorLogLevelHandler implements LogLevelHandler {
private final String LOGGER_NAME = ""; // root logger
Context context = Context.getInstance();
@Override
public String getName() {
return "Connector";
}
@Override
public Logger getLogger() {
return Logger.getLogger(LOGGER_NAME);
}
@Override
public void persistLevel(Level level) throws ConnectorManagerException {
File confFile = new File(new File(context.getCommonDirPath(), "classes"),
"logging.properties");
if (!persistLevel(level, confFile)) {
String filename = System.getProperty("java.util.logging.config.file");
if (!Strings.isNullOrEmpty(filename)) {
persistLevel(level, new File(filename));
}
}
}
/** Returns true if Level was successfully persisted. */
private boolean persistLevel(Level level, File confFile)
throws ConnectorManagerException {
if (confFile.canRead() && confFile.canWrite()) {
try {
Properties props = loadProperties(confFile);
props.setProperty(LOGGER_NAME + ".level", level.getName());
storeProperties(confFile, props);
} catch (IOException e) {
throw new ConnectorManagerException(
"Failed to save logging properties", e);
}
return true;
} else {
return false;
}
}
private Properties loadProperties(File propFile) throws IOException {
Properties props = new Properties();
props.load(new FileInputStream(propFile));
return props;
}
private void storeProperties(File propFile, Properties props)
throws IOException {
File backupFile =
new File(propFile.getParent(), propFile.getName() + ".bak");
// Back up the existing logging.properties file, because
// Properties.store() makes a mess of the output file.
if (!backupFile.exists()) {
propFile.renameTo(backupFile);
}
// TODO: Try to preserve logging.properties comments and order.
props.store(new FileOutputStream(propFile),
"Modified by Connector Manager LogLevel Servlet");
}
}
/**
* A LogLevelHandler for feedlogs, as configured in applicationContext.properties.
*/
private static class FeedLogLevelHandler implements LogLevelHandler {
Context context = Context.getInstance();
@Override
public String getName() {
return "Feed";
}
@Override
public Logger getLogger() {
return (Logger) context.getApplicationContext()
.getBean("FeedWrapperLogger", Logger.class);
}
@Override
public void persistLevel(Level level) {
try {
Properties props = context.getConnectorManagerProperties();
props.setProperty("feedLoggingLevel", level.getName());
context.storeConnectorManagerProperties(props);
} catch (PropertiesException pe) {
LOGGER.log(Level.WARNING, "Failed to save Connector Logging Level", pe);
}
}
}
}