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