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 */
018package org.apache.hadoop.hbase.http.log;
019
020import java.io.BufferedReader;
021import java.io.FileNotFoundException;
022import java.io.IOException;
023import java.io.InputStreamReader;
024import java.io.PrintWriter;
025import java.net.URL;
026import java.net.URLConnection;
027import java.util.Objects;
028import java.util.regex.Pattern;
029import javax.net.ssl.HttpsURLConnection;
030import javax.net.ssl.SSLSocketFactory;
031import javax.servlet.ServletException;
032import javax.servlet.http.HttpServlet;
033import javax.servlet.http.HttpServletRequest;
034import javax.servlet.http.HttpServletResponse;
035import org.apache.hadoop.HadoopIllegalArgumentException;
036import org.apache.hadoop.conf.Configuration;
037import org.apache.hadoop.conf.Configured;
038import org.apache.hadoop.hbase.http.HttpServer;
039import org.apache.hadoop.hbase.logging.Log4jUtils;
040import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
041import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
042import org.apache.hadoop.security.ssl.SSLFactory;
043import org.apache.hadoop.util.ServletUtil;
044import org.apache.hadoop.util.Tool;
045import org.apache.yetus.audience.InterfaceAudience;
046import org.apache.yetus.audience.InterfaceStability;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting;
051import org.apache.hbase.thirdparty.com.google.common.base.Charsets;
052
053/**
054 * Change log level in runtime.
055 */
056@InterfaceAudience.Private
057public final class LogLevel {
058  private static final String USAGES = "\nUsage: General options are:\n"
059      + "\t[-getlevel <host:port> <classname> [-protocol (http|https)]\n"
060      + "\t[-setlevel <host:port> <classname> <level> [-protocol (http|https)]";
061
062  public static final String PROTOCOL_HTTP = "http";
063  public static final String PROTOCOL_HTTPS = "https";
064
065  /**
066   * A command line implementation
067   */
068  public static void main(String[] args) throws Exception {
069    CLI cli = new CLI(new Configuration());
070    System.exit(cli.run(args));
071  }
072
073  /**
074   * Valid command line options.
075   */
076  private enum Operations {
077    GETLEVEL,
078    SETLEVEL,
079    UNKNOWN
080  }
081
082  private static void printUsage() {
083    System.err.println(USAGES);
084    System.exit(-1);
085  }
086
087  public static boolean isValidProtocol(String protocol) {
088    return ((protocol.equals(PROTOCOL_HTTP) ||
089        protocol.equals(PROTOCOL_HTTPS)));
090  }
091
092  @VisibleForTesting
093  static class CLI extends Configured implements Tool {
094    private Operations operation = Operations.UNKNOWN;
095    private String protocol;
096    private String hostName;
097    private String className;
098    private String level;
099
100    CLI(Configuration conf) {
101      setConf(conf);
102    }
103
104    @Override
105    public int run(String[] args) throws Exception {
106      try {
107        parseArguments(args);
108        sendLogLevelRequest();
109      } catch (HadoopIllegalArgumentException e) {
110        printUsage();
111      }
112      return 0;
113    }
114
115    /**
116     * Send HTTP request to the daemon.
117     * @throws HadoopIllegalArgumentException if arguments are invalid.
118     * @throws Exception if unable to connect
119     */
120    private void sendLogLevelRequest()
121        throws HadoopIllegalArgumentException, Exception {
122      switch (operation) {
123        case GETLEVEL:
124          doGetLevel();
125          break;
126        case SETLEVEL:
127          doSetLevel();
128          break;
129        default:
130          throw new HadoopIllegalArgumentException(
131              "Expect either -getlevel or -setlevel");
132      }
133    }
134
135    public void parseArguments(String[] args) throws
136        HadoopIllegalArgumentException {
137      if (args.length == 0) {
138        throw new HadoopIllegalArgumentException("No arguments specified");
139      }
140      int nextArgIndex = 0;
141      while (nextArgIndex < args.length) {
142        switch (args[nextArgIndex]) {
143          case "-getlevel":
144            nextArgIndex = parseGetLevelArgs(args, nextArgIndex);
145            break;
146          case "-setlevel":
147            nextArgIndex = parseSetLevelArgs(args, nextArgIndex);
148            break;
149          case "-protocol":
150            nextArgIndex = parseProtocolArgs(args, nextArgIndex);
151            break;
152          default:
153            throw new HadoopIllegalArgumentException(
154                "Unexpected argument " + args[nextArgIndex]);
155        }
156      }
157
158      // if operation is never specified in the arguments
159      if (operation == Operations.UNKNOWN) {
160        throw new HadoopIllegalArgumentException(
161            "Must specify either -getlevel or -setlevel");
162      }
163
164      // if protocol is unspecified, set it as http.
165      if (protocol == null) {
166        protocol = PROTOCOL_HTTP;
167      }
168    }
169
170    private int parseGetLevelArgs(String[] args, int index) throws
171        HadoopIllegalArgumentException {
172      // fail if multiple operations are specified in the arguments
173      if (operation != Operations.UNKNOWN) {
174        throw new HadoopIllegalArgumentException("Redundant -getlevel command");
175      }
176      // check number of arguments is sufficient
177      if (index + 2 >= args.length) {
178        throw new HadoopIllegalArgumentException("-getlevel needs two parameters");
179      }
180      operation = Operations.GETLEVEL;
181      hostName = args[index + 1];
182      className = args[index + 2];
183      return index + 3;
184    }
185
186    private int parseSetLevelArgs(String[] args, int index) throws
187        HadoopIllegalArgumentException {
188      // fail if multiple operations are specified in the arguments
189      if (operation != Operations.UNKNOWN) {
190        throw new HadoopIllegalArgumentException("Redundant -setlevel command");
191      }
192      // check number of arguments is sufficient
193      if (index + 3 >= args.length) {
194        throw new HadoopIllegalArgumentException("-setlevel needs three parameters");
195      }
196      operation = Operations.SETLEVEL;
197      hostName = args[index + 1];
198      className = args[index + 2];
199      level = args[index + 3];
200      return index + 4;
201    }
202
203    private int parseProtocolArgs(String[] args, int index) throws
204        HadoopIllegalArgumentException {
205      // make sure only -protocol is specified
206      if (protocol != null) {
207        throw new HadoopIllegalArgumentException(
208            "Redundant -protocol command");
209      }
210      // check number of arguments is sufficient
211      if (index + 1 >= args.length) {
212        throw new HadoopIllegalArgumentException(
213            "-protocol needs one parameter");
214      }
215      // check protocol is valid
216      protocol = args[index + 1];
217      if (!isValidProtocol(protocol)) {
218        throw new HadoopIllegalArgumentException(
219            "Invalid protocol: " + protocol);
220      }
221      return index + 2;
222    }
223
224    /**
225     * Send HTTP request to get log level.
226     *
227     * @throws HadoopIllegalArgumentException if arguments are invalid.
228     * @throws Exception if unable to connect
229     */
230    private void doGetLevel() throws Exception {
231      process(protocol + "://" + hostName + "/logLevel?log=" + className);
232    }
233
234    /**
235     * Send HTTP request to set log level.
236     *
237     * @throws HadoopIllegalArgumentException if arguments are invalid.
238     * @throws Exception if unable to connect
239     */
240    private void doSetLevel() throws Exception {
241      process(protocol + "://" + hostName + "/logLevel?log=" + className
242          + "&level=" + level);
243    }
244
245    /**
246     * Connect to the URL. Supports HTTP and supports SPNEGO
247     * authentication. It falls back to simple authentication if it fails to
248     * initiate SPNEGO.
249     *
250     * @param url the URL address of the daemon servlet
251     * @return a connected connection
252     * @throws Exception if it can not establish a connection.
253     */
254    private URLConnection connect(URL url) throws Exception {
255      AuthenticatedURL.Token token = new AuthenticatedURL.Token();
256      AuthenticatedURL aUrl;
257      SSLFactory clientSslFactory;
258      URLConnection connection;
259      // If https is chosen, configures SSL client.
260      if (PROTOCOL_HTTPS.equals(url.getProtocol())) {
261        clientSslFactory = new SSLFactory(SSLFactory.Mode.CLIENT, this.getConf());
262        clientSslFactory.init();
263        SSLSocketFactory sslSocketF = clientSslFactory.createSSLSocketFactory();
264
265        aUrl = new AuthenticatedURL(new KerberosAuthenticator(), clientSslFactory);
266        connection = aUrl.openConnection(url, token);
267        HttpsURLConnection httpsConn = (HttpsURLConnection) connection;
268        httpsConn.setSSLSocketFactory(sslSocketF);
269      } else {
270        aUrl = new AuthenticatedURL(new KerberosAuthenticator());
271        connection = aUrl.openConnection(url, token);
272      }
273      connection.connect();
274      return connection;
275    }
276
277    /**
278     * Configures the client to send HTTP request to the URL.
279     * Supports SPENGO for authentication.
280     * @param urlString URL and query string to the daemon's web UI
281     * @throws Exception if unable to connect
282     */
283    private void process(String urlString) throws Exception {
284      URL url = new URL(urlString);
285      System.out.println("Connecting to " + url);
286
287      URLConnection connection = connect(url);
288
289      // read from the servlet
290
291      try (InputStreamReader streamReader =
292            new InputStreamReader(connection.getInputStream(), Charsets.UTF_8);
293           BufferedReader bufferedReader = new BufferedReader(streamReader)) {
294        bufferedReader.lines().filter(Objects::nonNull).filter(line -> line.startsWith(MARKER))
295            .forEach(line -> System.out.println(TAG.matcher(line).replaceAll("")));
296      } catch (IOException ioe) {
297        System.err.println("" + ioe);
298      }
299    }
300  }
301
302  private static final String MARKER = "<!-- OUTPUT -->";
303  private static final Pattern TAG = Pattern.compile("<[^>]*>");
304
305  /**
306   * A servlet implementation
307   */
308  @InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"})
309  @InterfaceStability.Unstable
310  public static class Servlet extends HttpServlet {
311    private static final long serialVersionUID = 1L;
312
313    @Override
314    public void doGet(HttpServletRequest request, HttpServletResponse response)
315            throws ServletException, IOException {
316      // Do the authorization
317      if (!HttpServer.hasAdministratorAccess(getServletContext(), request,
318          response)) {
319        return;
320      }
321      // Disallow modification of the LogLevel if explicitly set to readonly
322      Configuration conf = (Configuration) getServletContext().getAttribute(
323          HttpServer.CONF_CONTEXT_ATTRIBUTE);
324      if (conf.getBoolean("hbase.master.ui.readonly", false)) {
325        response.sendError(HttpServletResponse.SC_FORBIDDEN, "Modification of HBase via"
326            + " the UI is disallowed in configuration.");
327        return;
328      }
329      response.setContentType("text/html");
330      PrintWriter out;
331      try {
332        String headerPath = "header.jsp?pageTitle=Log Level";
333        request.getRequestDispatcher(headerPath).include(request, response);
334        out = response.getWriter();
335      } catch (FileNotFoundException e) {
336        // in case file is not found fall back to old design
337        out = ServletUtil.initHTML(response, "Log Level");
338      }
339      out.println(FORMS);
340
341      String logName = ServletUtil.getParameter(request, "log");
342      String level = ServletUtil.getParameter(request, "level");
343
344      if (logName != null) {
345        out.println("<p>Results:</p>");
346        out.println(MARKER
347            + "Submitted Log Name: <b>" + logName + "</b><br />");
348
349        Logger log = LoggerFactory.getLogger(logName);
350        out.println(MARKER
351            + "Log Class: <b>" + log.getClass().getName() +"</b><br />");
352        if (level != null) {
353          out.println(MARKER + "Submitted Level: <b>" + level + "</b><br />");
354        }
355        process(log, level, out);
356      }
357
358      try {
359        String footerPath = "footer.jsp";
360        out.println("</div>");
361        request.getRequestDispatcher(footerPath).include(request, response);
362      } catch (FileNotFoundException e) {
363        out.println(ServletUtil.HTML_TAIL);
364      }
365      out.close();
366    }
367
368    static final String FORMS = "<div class='container-fluid content'>\n"
369        + "<div class='row inner_header'>\n" + "<div class='page-header'>\n"
370        + "<h1>Get/Set Log Level</h1>\n" + "</div>\n" + "</div>\n" + "Actions:" + "<p>"
371        + "<center>\n" + "<table class='table' style='border: 0;' width='95%' >\n" + "<tr>\n"
372        + "<form>\n" + "<td class='centered'>\n"
373        + "<input style='font-size: 12pt; width: 10em' type='submit' value='Get Log Level'"
374        + " class='btn' />\n" + "</td>\n" + "<td style='text-align: center;'>\n"
375        + "<input type='text' name='log' size='50' required='required'"
376        + " placeholder='Log Name (required)' />\n" + "</td>\n" + "<td width=\"40%\">"
377        + "Get the current log level for the specified log name." + "</td>\n" + "</form>\n"
378        + "</tr>\n" + "<tr>\n" + "<form>\n" + "<td class='centered'>\n"
379        + "<input style='font-size: 12pt; width: 10em' type='submit'"
380        + " value='Set Log Level' class='btn' />\n" + "</td>\n"
381        + "<td style='text-align: center;'>\n"
382        + "<input type='text' name='log' size='50' required='required'"
383        + " placeholder='Log Name (required)' />\n"
384        + "<input type='text' name='level' size='50' required='required'"
385        + " placeholder='Log Level (required)' />\n" + "</td>\n" + "<td width=\"40%\" style=\"\">"
386        + "Set the specified log level for the specified log name." + "</td>\n" + "</form>\n"
387        + "</tr>\n" + "</table>\n" + "</center>\n" + "</p>\n" + "<hr/>\n";
388
389    private static void process(Logger logger, String levelName, PrintWriter out) {
390      if (levelName != null) {
391        try {
392          Log4jUtils.setLogLevel(logger.getName(), levelName);
393          out.println(MARKER + "<div class='text-success'>" + "Setting Level to <strong>" +
394            levelName + "</strong> ...<br />" + "</div>");
395        } catch (IllegalArgumentException e) {
396          out.println(MARKER + "<div class='text-danger'>" + "Bad level : <strong>" + levelName +
397            "</strong><br />" + "</div>");
398        }
399      }
400      out.println(MARKER + "Effective level: <b>" + Log4jUtils.getEffectiveLevel(logger.getName()) +
401        "</b><br />");
402    }
403  }
404
405  private LogLevel() {}
406}