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