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}