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