| /* |
| * 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.hbase.http.log; |
| |
| import java.io.BufferedReader; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.io.PrintWriter; |
| import java.net.HttpURLConnection; |
| import java.net.URL; |
| import java.nio.charset.StandardCharsets; |
| import java.util.Objects; |
| import java.util.regex.Pattern; |
| import javax.net.ssl.HttpsURLConnection; |
| import javax.net.ssl.SSLSocketFactory; |
| import javax.servlet.ServletException; |
| import javax.servlet.http.HttpServlet; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| import org.apache.hadoop.HadoopIllegalArgumentException; |
| import org.apache.hadoop.conf.Configuration; |
| import org.apache.hadoop.conf.Configured; |
| import org.apache.hadoop.hbase.http.HttpServer; |
| import org.apache.hadoop.hbase.logging.Log4jUtils; |
| import org.apache.hadoop.security.authentication.client.AuthenticatedURL; |
| import org.apache.hadoop.security.authentication.client.KerberosAuthenticator; |
| import org.apache.hadoop.security.ssl.SSLFactory; |
| import org.apache.hadoop.util.HttpExceptionUtils; |
| import org.apache.hadoop.util.ServletUtil; |
| import org.apache.hadoop.util.Tool; |
| import org.apache.yetus.audience.InterfaceAudience; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * Change log level in runtime. |
| */ |
| @InterfaceAudience.Private |
| public final class LogLevel { |
| private static final String USAGES = "\nUsage: General options are:\n" |
| + "\t[-getlevel <host:port> <classname> [-protocol (http|https)]\n" |
| + "\t[-setlevel <host:port> <classname> <level> [-protocol (http|https)]"; |
| |
| public static final String PROTOCOL_HTTP = "http"; |
| public static final String PROTOCOL_HTTPS = "https"; |
| |
| public static final String READONLY_LOGGERS_CONF_KEY = "hbase.ui.logLevels.readonly.loggers"; |
| |
| /** |
| * A command line implementation |
| */ |
| public static void main(String[] args) throws Exception { |
| CLI cli = new CLI(new Configuration()); |
| System.exit(cli.run(args)); |
| } |
| |
| /** |
| * Valid command line options. |
| */ |
| private enum Operations { |
| GETLEVEL, |
| SETLEVEL, |
| UNKNOWN |
| } |
| |
| private static void printUsage() { |
| System.err.println(USAGES); |
| System.exit(-1); |
| } |
| |
| public static boolean isValidProtocol(String protocol) { |
| return protocol.equals(PROTOCOL_HTTP) || protocol.equals(PROTOCOL_HTTPS); |
| } |
| |
| static class CLI extends Configured implements Tool { |
| private Operations operation = Operations.UNKNOWN; |
| private String protocol; |
| private String hostName; |
| private String className; |
| private String level; |
| |
| CLI(Configuration conf) { |
| setConf(conf); |
| } |
| |
| @Override |
| public int run(String[] args) throws Exception { |
| try { |
| parseArguments(args); |
| sendLogLevelRequest(); |
| } catch (HadoopIllegalArgumentException e) { |
| printUsage(); |
| } |
| return 0; |
| } |
| |
| /** |
| * Send HTTP request to the daemon. |
| * @throws HadoopIllegalArgumentException if arguments are invalid. |
| * @throws Exception if unable to connect |
| */ |
| private void sendLogLevelRequest() throws HadoopIllegalArgumentException, Exception { |
| switch (operation) { |
| case GETLEVEL: |
| doGetLevel(); |
| break; |
| case SETLEVEL: |
| doSetLevel(); |
| break; |
| default: |
| throw new HadoopIllegalArgumentException("Expect either -getlevel or -setlevel"); |
| } |
| } |
| |
| public void parseArguments(String[] args) throws HadoopIllegalArgumentException { |
| if (args.length == 0) { |
| throw new HadoopIllegalArgumentException("No arguments specified"); |
| } |
| int nextArgIndex = 0; |
| while (nextArgIndex < args.length) { |
| switch (args[nextArgIndex]) { |
| case "-getlevel": |
| nextArgIndex = parseGetLevelArgs(args, nextArgIndex); |
| break; |
| case "-setlevel": |
| nextArgIndex = parseSetLevelArgs(args, nextArgIndex); |
| break; |
| case "-protocol": |
| nextArgIndex = parseProtocolArgs(args, nextArgIndex); |
| break; |
| default: |
| throw new HadoopIllegalArgumentException("Unexpected argument " + args[nextArgIndex]); |
| } |
| } |
| |
| // if operation is never specified in the arguments |
| if (operation == Operations.UNKNOWN) { |
| throw new HadoopIllegalArgumentException("Must specify either -getlevel or -setlevel"); |
| } |
| |
| // if protocol is unspecified, set it as http. |
| if (protocol == null) { |
| protocol = PROTOCOL_HTTP; |
| } |
| } |
| |
| private int parseGetLevelArgs(String[] args, int index) throws HadoopIllegalArgumentException { |
| // fail if multiple operations are specified in the arguments |
| if (operation != Operations.UNKNOWN) { |
| throw new HadoopIllegalArgumentException("Redundant -getlevel command"); |
| } |
| // check number of arguments is sufficient |
| if (index + 2 >= args.length) { |
| throw new HadoopIllegalArgumentException("-getlevel needs two parameters"); |
| } |
| operation = Operations.GETLEVEL; |
| hostName = args[index + 1]; |
| className = args[index + 2]; |
| return index + 3; |
| } |
| |
| private int parseSetLevelArgs(String[] args, int index) throws HadoopIllegalArgumentException { |
| // fail if multiple operations are specified in the arguments |
| if (operation != Operations.UNKNOWN) { |
| throw new HadoopIllegalArgumentException("Redundant -setlevel command"); |
| } |
| // check number of arguments is sufficient |
| if (index + 3 >= args.length) { |
| throw new HadoopIllegalArgumentException("-setlevel needs three parameters"); |
| } |
| operation = Operations.SETLEVEL; |
| hostName = args[index + 1]; |
| className = args[index + 2]; |
| level = args[index + 3]; |
| return index + 4; |
| } |
| |
| private int parseProtocolArgs(String[] args, int index) throws HadoopIllegalArgumentException { |
| // make sure only -protocol is specified |
| if (protocol != null) { |
| throw new HadoopIllegalArgumentException("Redundant -protocol command"); |
| } |
| // check number of arguments is sufficient |
| if (index + 1 >= args.length) { |
| throw new HadoopIllegalArgumentException("-protocol needs one parameter"); |
| } |
| // check protocol is valid |
| protocol = args[index + 1]; |
| if (!isValidProtocol(protocol)) { |
| throw new HadoopIllegalArgumentException("Invalid protocol: " + protocol); |
| } |
| return index + 2; |
| } |
| |
| /** |
| * Send HTTP request to get log level. |
| * @throws HadoopIllegalArgumentException if arguments are invalid. |
| * @throws Exception if unable to connect |
| */ |
| private void doGetLevel() throws Exception { |
| process(protocol + "://" + hostName + "/logLevel?log=" + className); |
| } |
| |
| /** |
| * Send HTTP request to set log level. |
| * @throws HadoopIllegalArgumentException if arguments are invalid. |
| * @throws Exception if unable to connect |
| */ |
| private void doSetLevel() throws Exception { |
| process(protocol + "://" + hostName + "/logLevel?log=" + className + "&level=" + level); |
| } |
| |
| /** |
| * Connect to the URL. Supports HTTP and supports SPNEGO authentication. It falls back to simple |
| * authentication if it fails to initiate SPNEGO. |
| * @param url the URL address of the daemon servlet |
| * @return a connected connection |
| * @throws Exception if it can not establish a connection. |
| */ |
| private HttpURLConnection connect(URL url) throws Exception { |
| AuthenticatedURL.Token token = new AuthenticatedURL.Token(); |
| AuthenticatedURL aUrl; |
| SSLFactory clientSslFactory; |
| HttpURLConnection connection; |
| // If https is chosen, configures SSL client. |
| if (PROTOCOL_HTTPS.equals(url.getProtocol())) { |
| clientSslFactory = new SSLFactory(SSLFactory.Mode.CLIENT, this.getConf()); |
| clientSslFactory.init(); |
| SSLSocketFactory sslSocketF = clientSslFactory.createSSLSocketFactory(); |
| |
| aUrl = new AuthenticatedURL(new KerberosAuthenticator(), clientSslFactory); |
| connection = aUrl.openConnection(url, token); |
| HttpsURLConnection httpsConn = (HttpsURLConnection) connection; |
| httpsConn.setSSLSocketFactory(sslSocketF); |
| } else { |
| aUrl = new AuthenticatedURL(new KerberosAuthenticator()); |
| connection = aUrl.openConnection(url, token); |
| } |
| connection.connect(); |
| return connection; |
| } |
| |
| /** |
| * Configures the client to send HTTP request to the URL. Supports SPENGO for authentication. |
| * @param urlString URL and query string to the daemon's web UI |
| * @throws Exception if unable to connect |
| */ |
| private void process(String urlString) throws Exception { |
| URL url = new URL(urlString); |
| System.out.println("Connecting to " + url); |
| |
| HttpURLConnection connection = connect(url); |
| |
| HttpExceptionUtils.validateResponse(connection, 200); |
| |
| // read from the servlet |
| |
| try ( |
| InputStreamReader streamReader = |
| new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8); |
| BufferedReader bufferedReader = new BufferedReader(streamReader)) { |
| bufferedReader.lines().filter(Objects::nonNull).filter(line -> line.startsWith(MARKER)) |
| .forEach(line -> System.out.println(TAG.matcher(line).replaceAll(""))); |
| } catch (IOException ioe) { |
| System.err.println("" + ioe); |
| } |
| } |
| } |
| |
| private static final String MARKER = "<!-- OUTPUT -->"; |
| private static final Pattern TAG = Pattern.compile("<[^>]*>"); |
| |
| /** |
| * A servlet implementation |
| */ |
| @InterfaceAudience.Private |
| public static class Servlet extends HttpServlet { |
| private static final long serialVersionUID = 1L; |
| |
| @Override |
| public void doGet(HttpServletRequest request, HttpServletResponse response) |
| throws ServletException, IOException { |
| // Do the authorization |
| if (!HttpServer.hasAdministratorAccess(getServletContext(), request, response)) { |
| return; |
| } |
| // Disallow modification of the LogLevel if explicitly set to readonly |
| Configuration conf = |
| (Configuration) getServletContext().getAttribute(HttpServer.CONF_CONTEXT_ATTRIBUTE); |
| if (conf.getBoolean("hbase.master.ui.readonly", false)) { |
| sendError(response, HttpServletResponse.SC_FORBIDDEN, |
| "Modification of HBase via the UI is disallowed in configuration."); |
| return; |
| } |
| response.setContentType("text/html"); |
| PrintWriter out; |
| try { |
| String headerPath = "header.jsp?pageTitle=Log Level"; |
| request.getRequestDispatcher(headerPath).include(request, response); |
| out = response.getWriter(); |
| } catch (FileNotFoundException e) { |
| // in case file is not found fall back to old design |
| out = ServletUtil.initHTML(response, "Log Level"); |
| } |
| out.println(FORMS); |
| |
| String logName = ServletUtil.getParameter(request, "log"); |
| String level = ServletUtil.getParameter(request, "level"); |
| |
| String[] readOnlyLogLevels = conf.getStrings(READONLY_LOGGERS_CONF_KEY); |
| |
| if (logName != null) { |
| out.println("<p>Results:</p>"); |
| out.println(MARKER + "Submitted Log Name: <b>" + logName + "</b><br />"); |
| |
| Logger log = LoggerFactory.getLogger(logName); |
| out.println(MARKER + "Log Class: <b>" + log.getClass().getName() + "</b><br />"); |
| if (level != null) { |
| if (!isLogLevelChangeAllowed(logName, readOnlyLogLevels)) { |
| sendError(response, HttpServletResponse.SC_PRECONDITION_FAILED, |
| "Modification of logger " + logName + " is disallowed in configuration."); |
| return; |
| } |
| |
| out.println(MARKER + "Submitted Level: <b>" + level + "</b><br />"); |
| } |
| process(log, level, out); |
| } |
| |
| try { |
| String footerPath = "footer.jsp"; |
| out.println("</div>"); |
| request.getRequestDispatcher(footerPath).include(request, response); |
| } catch (FileNotFoundException e) { |
| out.println(ServletUtil.HTML_TAIL); |
| } |
| out.close(); |
| } |
| |
| private boolean isLogLevelChangeAllowed(String logger, String[] readOnlyLogLevels) { |
| if (readOnlyLogLevels == null) { |
| return true; |
| } |
| for (String readOnlyLogLevel : readOnlyLogLevels) { |
| if (logger.startsWith(readOnlyLogLevel)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| private void sendError(HttpServletResponse response, int code, String message) |
| throws IOException { |
| response.setStatus(code, message); |
| response.sendError(code, message); |
| } |
| |
| static final String FORMS = "<div class='container-fluid content'>\n" |
| + "<div class='row inner_header top_header'>\n" + "<div class='page-header'>\n" |
| + "<h1>Get/Set Log Level</h1>\n" + "</div>\n" + "</div>\n" + "Actions:" + "<p>" + "<center>\n" |
| + "<table class='table' style='border: 0;' width='95%' >\n" + "<tr>\n" + "<form>\n" |
| + "<td class='centered'>\n" |
| + "<input style='font-size: 12pt; width: 10em' type='submit' value='Get Log Level'" |
| + " class='btn' />\n" + "</td>\n" + "<td style='text-align: center;'>\n" |
| + "<input type='text' name='log' size='50' required='required'" |
| + " placeholder='Log Name (required)' />\n" + "</td>\n" + "<td width=\"40%\">" |
| + "Get the current log level for the specified log name." + "</td>\n" + "</form>\n" |
| + "</tr>\n" + "<tr>\n" + "<form>\n" + "<td class='centered'>\n" |
| + "<input style='font-size: 12pt; width: 10em' type='submit'" |
| + " value='Set Log Level' class='btn' />\n" + "</td>\n" + "<td style='text-align: center;'>\n" |
| + "<input type='text' name='log' size='50' required='required'" |
| + " placeholder='Log Name (required)' />\n" |
| + "<input type='text' name='level' size='50' required='required'" |
| + " placeholder='Log Level (required)' />\n" + "</td>\n" + "<td width=\"40%\" style=\"\">" |
| + "Set the specified log level for the specified log name." + "</td>\n" + "</form>\n" |
| + "</tr>\n" + "</table>\n" + "</center>\n" + "</p>\n" + "<hr/>\n"; |
| |
| private static void process(Logger logger, String levelName, PrintWriter out) { |
| if (levelName != null) { |
| try { |
| Log4jUtils.setLogLevel(logger.getName(), levelName); |
| out.println(MARKER + "<div class='text-success'>" + "Setting Level to <strong>" |
| + levelName + "</strong> ...<br />" + "</div>"); |
| } catch (IllegalArgumentException e) { |
| out.println(MARKER + "<div class='text-danger'>" + "Bad level : <strong>" + levelName |
| + "</strong><br />" + "</div>"); |
| } |
| } |
| out.println(MARKER + "Effective level: <b>" + Log4jUtils.getEffectiveLevel(logger.getName()) |
| + "</b><br />"); |
| } |
| } |
| |
| private LogLevel() { |
| } |
| } |