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