| // *************************************************************************************************************************** |
| // * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * |
| // * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * |
| // * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * |
| // * with the License. You may obtain a copy of the License at * |
| // * * |
| // * http://www.apache.org/licenses/LICENSE-2.0 * |
| // * * |
| // * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * |
| // * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * |
| // * specific language governing permissions and limitations under the License. * |
| // *************************************************************************************************************************** |
| package org.apache.juneau.rest.logging; |
| |
| import static org.apache.juneau.internal.ObjectUtils.*; |
| import static org.apache.juneau.internal.StringUtils.*; |
| import static org.apache.juneau.rest.logging.RestLoggingDetail.*; |
| import static org.apache.juneau.Enablement.*; |
| import static org.apache.juneau.SystemProperties.*; |
| import static java.util.logging.Level.*; |
| |
| import java.util.*; |
| import java.util.function.*; |
| import java.util.logging.*; |
| |
| import javax.servlet.http.*; |
| |
| import org.apache.juneau.*; |
| import org.apache.juneau.collections.*; |
| import org.apache.juneau.internal.*; |
| import org.apache.juneau.json.*; |
| import org.apache.juneau.rest.*; |
| import org.apache.juneau.rest.annotation.*; |
| import org.apache.juneau.rest.util.*; |
| import org.apache.juneau.utils.*; |
| |
| /** |
| * Interface class used for logging HTTP requests to the log file. |
| * |
| * <p> |
| * Provides the following capabilities: |
| * <ul> |
| * <li>Allows incoming HTTP requests to be logged at various {@link Enablement detail levels}. |
| * <li>Allows rules to be defined to handle request logging differently depending on the resulting status code. |
| * <li>Allows use of stack trace hashing to eliminate duplication of stack traces in log files. |
| * <li>Allows customization of handling of where requests are logged to. |
| * <li>Allows configuration via system properties or environment variables. |
| * </ul> |
| * |
| * <p> |
| * The following is an example of a logger that logs errors only when debugging is not enabled, and everything when |
| * logging is enabled. |
| * |
| * <h5 class='section'>Example:</h5> |
| * <p class='bcode w800'> |
| * RestLogger |
| * .<jsm>create</jsm>() |
| * .logger(<js>"MyLogger"</js>) <jc>// Use MyLogger Java logger.</jc> |
| * .normalRules( <jc>// Rules when debugging is not enabled.</jc> |
| * <jsm>createRule</jsm>() <jc>// Log 500+ errors with status-line and header information.</jc> |
| * .statusFilter(x -> x >= 500) |
| * .level(<jsf>SEVERE</jsf>) |
| * .requestDetail(<jsf>HEADER</jsf>) |
| * .responseDetail<jsf>(HEADER</jsf>) |
| * .build(), |
| * <jsm>createRule</jsm>() <jc>// Log 400-500 errors with just status-line information.</jc> |
| * .statusFilter(x -> x >= 400) |
| * .level(<jsf>WARNING</jsf>) |
| * .requestDetail(<jsf>STATUS_LINE</jsf>) |
| * .responseDetail(<jsf>STATUS_LINE</jsf>) |
| * .build() |
| * ) |
| * .debugRules( <jc>// Rules when debugging is enabled.</jc> |
| * <jsm>createRule</jsm>() <jc>// Log everything with full details.</jc> |
| * .level(<jsf>SEVERE</jsf>) |
| * .requestDetail(<jsf>ENTITY</jsf>) |
| * .responseDetail(<jsf>ENTITY</jsf>) |
| * .build() |
| * ) |
| * ; |
| * </p> |
| * |
| * <p> |
| * The {@link RestLoggerBuilder#build(Class)} method has been provided for easy extension of this class. |
| * |
| * <p> |
| * The following default implementations are also provided: |
| * <ul class='javatree'> |
| * <li class='jc'>{@link BasicRestLogger} - The default logger typically used. |
| * <li class='jc'>{@link BasicDisabledRestLogger} - A no-op logger if you want to turn off logging entirely. |
| * <li class='jc'>{@link BasicTestRestLogger} - A logger useful for testcases. |
| * <li class='jc'>{@link BasicTestCaptureRestLogger} - Useful for capturing log messages for testing logging itself. |
| * </ul> |
| * |
| * <ul class='seealso'> |
| * <li class='jf'>{@link RestContext#REST_callLogger} |
| * <li class='jf'>{@link RestContext#REST_callLoggerDefault} |
| * <li class='jf'>{@link RestContext#REST_debug} |
| * <li class='jf'>{@link RestContext#REST_debugOn} |
| * <li class='ja'>{@link Rest#debug} |
| * <li class='ja'>{@link RestMethod#debug} |
| * <li class='link'>{@doc RestLoggingAndDebugging} |
| * </ul> |
| */ |
| public class RestLogger { |
| |
| /** Represents no logger */ |
| public static final class Null extends RestLogger { |
| Null(RestLoggerBuilder builder) { |
| super(create()); |
| } |
| } |
| |
| private static final RestLoggerRule DEFAULT_RULE = RestLoggerRule.create().build(); |
| |
| /** |
| * System property name for the default logger name to use for {@link RestLogger} objects. |
| * <p> |
| * Can also use a <c>JUNEAU_RESTLOGGER_LOGGER</c> environment variable. |
| * <p> |
| * If not specified, the default is <js>"global"</js>. |
| */ |
| public static final String SP_logger = "juneau.restLogger.logger"; |
| |
| /** |
| * System property name for the default enablement setting for {@link RestLogger} objects. |
| * <p> |
| * Can also use a <c>JUNEAU_RESTLOGGER_ENABLED</c> environment variable. |
| * <p> |
| * The possible values are: |
| * <ul> |
| * <li>{@link Enablement#ALWAYS "ALWAYS"} (default) - Logging is enabled. |
| * <li>{@link Enablement#NEVER "NEVER"} - Logging is disabled. |
| * <li>{@link Enablement#CONDITIONAL "CONDITIONALLY"} - Logging is enabled if it passes the {@link RestLoggerBuilder#enabledTest(Predicate)} test. |
| * </ul> |
| */ |
| public static final String SP_enabled = "juneau.restLogger.enabled"; |
| |
| /** |
| * System property name for the default request detail setting for {@link RestLogger} objects. |
| * <p> |
| * Can also use a <c>JUNEAU_RESTLOGGER_REQUESTDETAIL</c> environment variable. |
| * <p> |
| * The possible values are: |
| * <ul> |
| * <li>{@link RestLoggingDetail#STATUS_LINE "STATUS_LINE"} (default) - Log only the status line. |
| * <li>{@link RestLoggingDetail#HEADER "HEADER"} - Log the status line and headers. |
| * <li>{@link RestLoggingDetail#ENTITY "ENTITY"} - Log the status line and headers and body if available. |
| * </ul> |
| */ |
| public static final String SP_requestDetail = "juneau.restLogger.requestDetail"; |
| |
| /** |
| * System property name for the default response detail setting for {@link RestLogger} objects. |
| * <p> |
| * Can also use a <c>JUNEAU_RESTLOGGER_RESPONSEDETAIL</c> environment variable. |
| * <p> |
| * The possible values are: |
| * <ul> |
| * <li>{@link RestLoggingDetail#STATUS_LINE "STATUS_LINE"} (default) - Log only the status line. |
| * <li>{@link RestLoggingDetail#HEADER "HEADER"} - Log the status line and headers. |
| * <li>{@link RestLoggingDetail#ENTITY "ENTITY"} - Log the status line and headers and body if available. |
| * </ul> |
| */ |
| public static final String SP_responseDetail = "juneau.restLogger.responseDetail"; |
| |
| /** |
| * System property name for the logging level setting for {@link RestLogger} objects. |
| * <p> |
| * Can also use a <c>JUNEAU_RESTLOGGER_LEVEL</c> environment variable. |
| * <p> |
| * The possible values are: |
| * <ul> |
| * <li>{@link Level#OFF "OFF"} (default) |
| * <li>{@link Level#SEVERE "SEVERE"} |
| * <li>{@link Level#WARNING "WARNING"} |
| * <li>{@link Level#INFO "INFO"} |
| * <li>{@link Level#CONFIG "CONFIG"} |
| * <li>{@link Level#FINE "FINE"} |
| * <li>{@link Level#FINER "FINER"} |
| * <li>{@link Level#FINEST "FINEST"} |
| * </ul> |
| */ |
| public static final String SP_level = "juneau.restLogger.level"; |
| |
| /** |
| * Creates a new builder for this object. |
| * |
| * @return A new builder for this object. |
| */ |
| public static RestLoggerBuilder create() { |
| return new RestLoggerBuilder(); |
| } |
| |
| /** |
| * Creates a new rule builder. |
| * |
| * @return A new rule builder. |
| */ |
| public static RestLoggerRuleBuilder createRule() { |
| return new RestLoggerRuleBuilder(); |
| } |
| |
| private final Logger logger; |
| private final StackTraceStore stackTraceStore; |
| private final RestLoggerRule[] normalRules, debugRules; |
| private final Enablement enabled; |
| private final Predicate<HttpServletRequest> enabledTest; |
| private final Level level; |
| private final RestLoggingDetail requestDetail, responseDetail; |
| |
| /** |
| * Constructor. |
| * |
| * @param builder The builder object. |
| */ |
| public RestLogger(RestLoggerBuilder builder) { |
| this.logger = firstNonNull(builder.logger, Logger.getLogger(getProperty(String.class, SP_logger, "global"))); |
| this.stackTraceStore = builder.stackTraceDatabase; |
| this.normalRules = builder.normalRules.toArray(new RestLoggerRule[builder.normalRules.size()]); |
| this.debugRules = builder.debugRules.toArray(new RestLoggerRule[builder.debugRules.size()]); |
| this.enabled = firstNonNull(builder.enabled, getProperty(Enablement.class, SP_enabled, ALWAYS)); |
| this.enabledTest = firstNonNull(builder.enabledTest, x -> false); |
| this.requestDetail = firstNonNull(builder.requestDetail, getProperty(RestLoggingDetail.class, SP_requestDetail, STATUS_LINE)); |
| this.responseDetail = firstNonNull(builder.responseDetail, getProperty(RestLoggingDetail.class, SP_responseDetail, STATUS_LINE)); |
| this.level = firstNonNull(builder.level, getProperty(Level.class, SP_level, OFF)); |
| } |
| |
| /** |
| * Called at the end of a servlet request to log the request. |
| * |
| * @param req The servlet request. |
| * @param res The servlet response. |
| */ |
| public void log(HttpServletRequest req, HttpServletResponse res) { |
| |
| RestLoggerRule rule = getRule(req, res); |
| |
| if (! isEnabled(rule, req)) |
| return; |
| |
| Level level = firstNonNull(rule.getLevel(), this.level); |
| |
| if (level == Level.OFF) |
| return; |
| |
| Throwable e = castOrNull(req.getAttribute("Exception"), Throwable.class); |
| Long execTime = castOrNull(req.getAttribute("ExecTime"), Long.class); |
| |
| RestLoggingDetail reqd = firstNonNull(rule.getRequestDetail(), requestDetail); |
| RestLoggingDetail resd = firstNonNull(rule.getResponseDetail(), responseDetail); |
| |
| String method = req.getMethod(); |
| int status = res.getStatus(); |
| String uri = req.getRequestURI(); |
| byte[] reqBody = getRequestBody(req); |
| byte[] resBody = getResponseBody(req, res); |
| |
| StringBuilder sb = new StringBuilder(); |
| |
| if (reqd != STATUS_LINE || resd != STATUS_LINE) |
| sb.append("\n=== HTTP Call (incoming) ======================================================\n"); |
| |
| StackTraceInfo sti = getStackTraceInfo(e); |
| |
| sb.append('[').append(status); |
| |
| if (sti != null) { |
| int count = sti.getCount(); |
| sb.append(',').append(sti.getHash()).append('.').append(count); |
| if (count > 1) |
| e = null; |
| } |
| |
| sb.append("] "); |
| |
| sb.append("HTTP ").append(method).append(' ').append(uri); |
| |
| if (reqd != STATUS_LINE || resd != STATUS_LINE) { |
| |
| if (reqd.isOneOf(HEADER, ENTITY)) { |
| String qs = req.getQueryString(); |
| if (qs != null) |
| sb.append('?').append(qs); |
| } |
| |
| if (reqBody != null && reqd.isOneOf(HEADER ,ENTITY)) |
| sb.append("\n\tRequest length: ").append(reqBody.length).append(" bytes"); |
| |
| if (resd.isOneOf(HEADER, ENTITY)) |
| sb.append("\n\tResponse code: ").append(status); |
| |
| if (resBody != null && resd.isOneOf(HEADER, ENTITY)) |
| sb.append("\n\tResponse length: ").append(resBody.length).append(" bytes"); |
| |
| if (execTime != null && resd.isOneOf(HEADER, ENTITY)) |
| sb.append("\n\tExec time: ").append(execTime).append("ms"); |
| |
| if (reqd.isOneOf(HEADER, ENTITY)) { |
| Enumeration<String> hh = req.getHeaderNames(); |
| if (hh.hasMoreElements()) { |
| sb.append("\n---Request Headers---"); |
| while (hh.hasMoreElements()) { |
| String h = hh.nextElement(); |
| sb.append("\n\t").append(h).append(": ").append(req.getHeader(h)); |
| } |
| } |
| } |
| |
| if (resd.isOneOf(HEADER, ENTITY)) { |
| Collection<String> hh = res.getHeaderNames(); |
| if (hh.size() > 0) { |
| sb.append("\n---Response Headers---"); |
| for (String h : hh) { |
| sb.append("\n\t").append(h).append(": ").append(res.getHeader(h)); |
| } |
| } |
| } |
| |
| if (reqBody != null && reqBody.length > 0 && reqd == ENTITY) { |
| try { |
| sb.append("\n---Request Body UTF-8---"); |
| sb.append("\n").append(new String(reqBody, IOUtils.UTF8)); |
| sb.append("\n---Request Body Hex---"); |
| sb.append("\n").append(toSpacedHex(reqBody)); |
| } catch (Exception e1) { |
| sb.append("\n").append(e1.getLocalizedMessage()); |
| } |
| } |
| |
| if (resBody != null && resBody.length > 0 && resd == ENTITY) { |
| try { |
| sb.append("\n---Response Body UTF-8---"); |
| sb.append("\n").append(new String(resBody, IOUtils.UTF8)); |
| sb.append("\n---Response Body Hex---"); |
| sb.append("\n").append(toSpacedHex(resBody)); |
| } catch (Exception e1) { |
| sb.append(e1.getLocalizedMessage()); |
| } |
| } |
| sb.append("\n=== END ======================================================================"); |
| } |
| |
| if (rule.isLogStackTrace() && e == null) |
| e = new Throwable("Stacktrace"); |
| |
| log(level, sb.toString(), e); |
| |
| } |
| |
| /** |
| * Given the specified servlet request/response, find the rule that applies to it. |
| * |
| * <p> |
| * This method can be overridden to provide specialized logic for finding rules. |
| * |
| * @param req The servlet request. |
| * @param res The servlet response. |
| * @return The applicable logging rule, or the default rule if not found. Never <jk>null</jk>. |
| */ |
| protected RestLoggerRule getRule(HttpServletRequest req, HttpServletResponse res) { |
| for (RestLoggerRule r : isDebug(req) ? debugRules : normalRules) |
| if (r.matches(req, res)) |
| return r; |
| return DEFAULT_RULE; |
| } |
| |
| /** |
| * Returns <jk>true</jk> if debug is enabled on this request. |
| * |
| * <p> |
| * Looks for the request attribute <js>"Debug"</js> to determine whether debug is enabled. |
| * |
| * <p> |
| * This method can be overridden to provide specialized logic for determining whether debug mode is enabled on a request. |
| * |
| * @param req The HTTP request being logged. |
| * @return <jk>true</jk> if debug is enabled on this request. |
| * @see RestContext#REST_debug |
| * @see RestContext#REST_debugOn |
| * @see Rest#debug() |
| * @see RestMethod#debug() |
| */ |
| protected boolean isDebug(HttpServletRequest req) { |
| return firstNonNull(castOrNull(req.getAttribute("Debug"), Boolean.class), false); |
| } |
| |
| /** |
| * Returns <jk>true</jk> if logging is enabled for this request. |
| * |
| * <p> |
| * Uses the enabled and enabled-test settings on the matched rule and this logger to determine whether a REST |
| * call should be logged. |
| * |
| * <p> |
| * This method can be overridden to provide specialized logic for determining whether a REST call should be logged. |
| * |
| * @param rule The first matching rule. Never <jk>null</jk>. |
| * @param req The HTTP request. |
| * @return <jk>true</jk> if logging is enabled for this request. |
| */ |
| protected boolean isEnabled(RestLoggerRule rule, HttpServletRequest req) { |
| Enablement enabled = firstNonNull(rule.getEnabled(), this.enabled); |
| Predicate<HttpServletRequest> enabledTest = firstNonNull(rule.getEnabledTest(), this.enabledTest); |
| return enabled.isEnabled(enabledTest.test(req)); |
| } |
| |
| |
| //----------------------------------------------------------------------------------------------------------------- |
| // Other methods |
| //----------------------------------------------------------------------------------------------------------------- |
| |
| /** |
| * Returns the logger to use for logging REST calls. |
| * |
| * <p> |
| * Returns the logger specified in the builder, or {@link Logger#getGlobal()} if it wasn't specified. |
| * |
| * <p> |
| * This method can be overridden in subclasses to provide a different logger. |
| * |
| * @return The logger to use for logging REST calls. |
| */ |
| protected Logger getLogger() { |
| return logger; |
| } |
| |
| /** |
| * Logs the specified message to the logger. |
| * |
| * <p> |
| * Subclasses can override this method to capture messages being sent to the logger and handle it differently. |
| * |
| * @param level The log level. |
| * @param msg The log message. |
| * @param e The exception. |
| */ |
| protected void log(Level level, String msg, Throwable e) { |
| getLogger().log(level, msg, e); |
| } |
| |
| private byte[] getRequestBody(HttpServletRequest req) { |
| if (req instanceof RestRequest) |
| req = ((RestRequest)req).getInner(); |
| if (req instanceof CachingHttpServletRequest) |
| return ((CachingHttpServletRequest)req).getBody(); |
| return castOrNull(req.getAttribute("RequestBody"), byte[].class); |
| } |
| |
| private byte[] getResponseBody(HttpServletRequest req, HttpServletResponse res) { |
| if (res instanceof RestResponse) |
| res = ((RestResponse)res).getInner(); |
| if (res instanceof CachingHttpServletResponse) |
| return ((CachingHttpServletResponse)res).getBody(); |
| return castOrNull(req.getAttribute("ResponseBody"), byte[].class); |
| } |
| |
| private StackTraceInfo getStackTraceInfo(Throwable e) { |
| if (e == null || stackTraceStore == null) |
| return null; |
| stackTraceStore.add(e); |
| return stackTraceStore.getStackTraceInfo(e); |
| } |
| |
| /** |
| * Returns the properties defined on this bean context as a simple map for debugging purposes. |
| * |
| * @return A new map containing the properties defined on this context. |
| */ |
| public OMap toMap() { |
| return OMap.create() |
| .a("logger", logger) |
| .a("stackTraceStore", stackTraceStore) |
| .a("enabled", enabled) |
| .a("level", level) |
| .a("requestDetail", requestDetail) |
| .a("responseDetail", responseDetail) |
| .a("normalRules", normalRules.length == 0 ? null : normalRules) |
| .a("debugRules", debugRules.length == 0 ? null : debugRules) |
| ; |
| } |
| |
| @Override /* Object */ |
| public String toString() { |
| return SimpleJsonSerializer.DEFAULT_READABLE.toString(toMap()); |
| } |
| } |