![JAR search and dependency download from the Maven repository](/logo.png)
org.apache.juneau.rest.logger.CallLogger Maven / Gradle / Ivy
// ***************************************************************************************************************************
// * 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.logger;
import static java.util.logging.Level.*;
import static org.apache.juneau.Enablement.*;
import static org.apache.juneau.collections.JsonMap.*;
import static org.apache.juneau.common.internal.StringUtils.*;
import static org.apache.juneau.internal.CollectionUtils.*;
import static org.apache.juneau.internal.ObjectUtils.*;
import static org.apache.juneau.internal.SystemEnv.*;
import static org.apache.juneau.rest.logger.CallLoggingDetail.*;
import java.util.*;
import java.util.function.*;
import java.util.logging.*;
import jakarta.servlet.http.*;
import org.apache.juneau.*;
import org.apache.juneau.common.internal.*;
import org.apache.juneau.cp.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.rest.annotation.*;
import org.apache.juneau.rest.stats.*;
import org.apache.juneau.rest.util.*;
/**
* Basic implementation of a {@link CallLogger} for logging HTTP requests.
*
*
* Provides the following capabilities:
*
* - Allows incoming HTTP requests to be logged at various {@link Enablement detail levels}.
*
- Allows rules to be defined to handle request logging differently depending on the resulting status code.
*
- Allows use of stack trace hashing to eliminate duplication of stack traces in log files.
*
- Allows customization of handling of where requests are logged to.
*
- Allows configuration via system properties or environment variables.
*
*
*
* The following is an example of a logger that logs errors only when debugging is not enabled, and everything when
* logging is enabled.
*
*
Example:
*
* CallLogger logger = CallLogger
* .create ()
* .logger("MyLogger" ) // Use MyLogger Java logger.
* .normalRules( // Rules when debugging is not enabled.
* createRule () // Log 500+ errors with status-line and header information.
* .statusFilter(x -> x >= 500)
* .level(SEVERE )
* .requestDetail(HEADER )
* .responseDetail(HEADER )
* .build(),
* createRule () // Log 400-500 errors with just status-line information.
* .statusFilter(x -> x >= 400)
* .level(WARNING )
* .requestDetail(STATUS_LINE )
* .responseDetail(STATUS_LINE )
* .build()
* )
* .debugRules( // Rules when debugging is enabled.
* createRule () // Log everything with full details.
* .level(SEVERE )
* .requestDetail(ENTITY )
* .responseDetail(ENTITY )
* .build()
* )
* .build()
* ;
*
*
* See Also:
* - {@link org.apache.juneau.rest.RestContext.Builder#callLogger()}
*
- {@link org.apache.juneau.rest.RestContext.Builder#debugEnablement()}
*
- {@link Rest#debug}
*
- {@link RestOp#debug}
*
- Logging / Debugging
*
*/
public class CallLogger {
//-----------------------------------------------------------------------------------------------------------------
// Static
//-----------------------------------------------------------------------------------------------------------------
private static final CallLoggerRule DEFAULT_RULE = CallLoggerRule.create(BeanStore.INSTANCE).build();
/** Represents no logger */
public abstract class Void extends CallLogger {
Void(BeanStore beanStore) {
super(beanStore);
}
}
/**
* System property name for the default logger name to use for {@link CallLogger} objects.
*
* Can also use a JUNEAU_RESTLOGGER_LOGGER environment variable.
*
* If not specified, the default is "global" .
*/
public static final String SP_logger = "juneau.restLogger.logger";
/**
* System property name for the default enablement setting for {@link CallLogger} objects.
*
* Can also use a JUNEAU_RESTLOGGER_ENABLED environment variable.
*
* The possible values are:
*
* - {@link Enablement#ALWAYS "ALWAYS"} (default) - Logging is enabled.
*
- {@link Enablement#NEVER "NEVER"} - Logging is disabled.
*
- {@link Enablement#CONDITIONAL "CONDITIONALLY"} - Logging is enabled if it passes the {@link Builder#enabledTest(Predicate)} test.
*
*/
public static final String SP_enabled = "juneau.restLogger.enabled";
/**
* System property name for the default request detail setting for {@link CallLogger} objects.
*
* Can also use a JUNEAU_RESTLOGGER_REQUESTDETAIL environment variable.
*
*
* - {@link CallLoggingDetail#STATUS_LINE "STATUS_LINE"} (default) - Log only the status line.
*
- {@link CallLoggingDetail#HEADER "HEADER"} - Log the status line and headers.
*
- {@link CallLoggingDetail#ENTITY "ENTITY"} - Log the status line and headers and content if available.
*
*/
public static final String SP_requestDetail = "juneau.restLogger.requestDetail";
/**
* System property name for the default response detail setting for {@link CallLogger} objects.
*
* Can also use a JUNEAU_RESTLOGGER_RESPONSEDETAIL environment variable.
*
*
* - {@link CallLoggingDetail#STATUS_LINE "STATUS_LINE"} (default) - Log only the status line.
*
- {@link CallLoggingDetail#HEADER "HEADER"} - Log the status line and headers.
*
- {@link CallLoggingDetail#ENTITY "ENTITY"} - Log the status line and headers and content if available.
*
*/
public static final String SP_responseDetail = "juneau.restLogger.responseDetail";
/**
* System property name for the logging level setting for {@link CallLogger} objects.
*
* Can also use a JUNEAU_RESTLOGGER_LEVEL environment variable.
*
*
* - {@link Level#OFF "OFF"} (default)
*
- {@link Level#SEVERE "SEVERE"}
*
- {@link Level#WARNING "WARNING"}
*
- {@link Level#INFO "INFO"}
*
- {@link Level#CONFIG "CONFIG"}
*
- {@link Level#FINE "FINE"}
*
- {@link Level#FINER "FINER"}
*
- {@link Level#FINEST "FINEST"}
*
*/
public static final String SP_level = "juneau.restLogger.level";
/**
* Static creator.
*
* @param beanStore The bean store to use for creating beans.
* @return A new builder for this object.
*/
public static Builder create(BeanStore beanStore) {
return new Builder(beanStore);
}
//-----------------------------------------------------------------------------------------------------------------
// Builder
//-----------------------------------------------------------------------------------------------------------------
/**
* Builder class.
*/
@FluentSetters
public static class Builder {
Logger logger;
ThrownStore thrownStore;
List normalRules = list(), debugRules = list();
Enablement enabled;
Predicate enabledTest;
CallLoggingDetail requestDetail, responseDetail;
Level level;
/**
* Constructor.
*
* @param beanStore The bean store to use for creating beans.
*/
protected Builder(BeanStore beanStore) {
logger = Logger.getLogger(env(SP_logger, "global"));
enabled = env(SP_enabled, ALWAYS);
enabledTest = x -> false;
requestDetail = env(SP_requestDetail, STATUS_LINE);
responseDetail = env(SP_responseDetail, STATUS_LINE);
level = env(SP_level).map(Level::parse).orElse(OFF);
}
//-------------------------------------------------------------------------------------------------------------
// Properties
//-------------------------------------------------------------------------------------------------------------
/**
* Specifies the logger to use for logging the request.
*
*
* If not specified, the logger name is determined in the following order:
*
* {@link CallLogger#SP_logger "juneau.restLogger.logger"} system property.
* {@link CallLogger#SP_logger "JUNEAU_RESTLOGGER_LOGGER"} environment variable.
* "global" .
*
*
*
* The {@link CallLogger#getLogger()} method can also be overridden to provide different logic.
*
* @param value
* The logger to use for logging the request.
* @return This object.
*/
public Builder logger(Logger value) {
logger = value;
return this;
}
/**
* Specifies the logger to use for logging the request.
*
*
* Shortcut for calling logger(Logger.getLogger (value)) .
*
*
* If not specified, the logger name is determined in the following order:
*
* {@link CallLogger#SP_logger "juneau.restLogger.logger"} system property.
* {@link CallLogger#SP_logger "JUNEAU_RESTLOGGER_LOGGER"} environment variable.
* "global" .
*
*
*
* The {@link CallLogger#getLogger()} method can also be overridden to provide different logic.
*
* @param value
* The logger to use for logging the request.
* @return This object.
*/
public Builder logger(String value) {
logger = value == null ? null :Logger.getLogger(value);
return this;
}
/**
* Same as {@link #logger(Logger)} but only sets the value if it's currently null .
*
* @param value The logger to use for logging the request.
* @return This object.
*/
public Builder loggerOnce(Logger value) {
if (logger == null)
logger = value;
return this;
}
/**
* Specifies the thrown exception store to use for getting stack trace information (hash IDs and occurrence counts).
*
* @param value
* The stack trace store.
*
If null , stack trace information will not be logged.
* @return This object.
*/
public Builder thrownStore(ThrownStore value) {
thrownStore = value;
return this;
}
/**
* Same as {@link #thrownStore(ThrownStore)} but only sets the value if it's currently null .
*
* @param value
* The stack trace store.
*
If null , stack trace information will not be logged.
* @return This object.
*/
public Builder thrownStoreOnce(ThrownStore value) {
if (thrownStore == null)
thrownStore = value;
return this;
}
/**
* Specifies the default logging enablement setting.
*
*
* This specifies the default logging enablement value if not set on the first matched rule or if no rules match.
*
*
* If not specified, the setting is determined via the following:
*
* {@link CallLogger#SP_enabled "juneau.restLogger.enabled"} system property.
* {@link CallLogger#SP_enabled "JUNEAU_RESTLOGGER_ENABLED"} environment variable.
* "ALWAYS" .
*
*
*
* - {@link Enablement#ALWAYS ALWAYS} (default) - Logging is enabled.
*
- {@link Enablement#NEVER NEVER} - Logging is disabled.
*
- {@link Enablement#CONDITIONAL CONDITIONALLY} - Logging is enabled if it passes the {@link #enabledTest(Predicate)} test.
*
*
*
* @param value
* The default enablement flag value. Can be null to use the default.
* @return This object.
*/
public Builder enabled(Enablement value) {
enabled = value;
return this;
}
/**
* Specifies the default logging enablement test predicate.
*
*
* This specifies the default logging enablement test if not set on the first matched rule or if no rules match.
*
*
* This setting has no effect if the enablement setting is not {@link Enablement#CONDITIONAL CONDITIONALLY}.
*
*
* The default if not specified is x -> false (never log).
*
* @param value
* The default enablement flag value. Can be null to use the default.
* @return This object.
*/
public Builder enabledTest(Predicate value) {
enabledTest = value;
return this;
}
/**
* Shortcut for calling enabled(NEVER ) .
*
* @return This object.
*/
public Builder disabled() {
return enabled(NEVER);
}
/**
* The default level of detail to log on a request.
*
*
* This specifies the default level of request detail if not set on the first matched rule or if no rules match.
*
*
* If not specified, the setting is determined via the following:
*
* {@link CallLogger#SP_requestDetail "juneau.restLogger.requestDetail"} system property.
* {@link CallLogger#SP_requestDetail "JUNEAU_RESTLOGGER_requestDetail"} environment variable.
* "STATUS_LINE" .
*
*
*
* - {@link CallLoggingDetail#STATUS_LINE STATUS_LINE} - Log only the status line.
*
- {@link CallLoggingDetail#HEADER HEADER} - Log the status line and headers.
*
- {@link CallLoggingDetail#ENTITY ENTITY} - Log the status line and headers and content if available.
*
*
* @param value
* The new value for this property, or null to use the default.
* @return This object.
*/
public Builder requestDetail(CallLoggingDetail value) {
requestDetail = value;
return this;
}
/**
* The default level of detail to log on a response.
*
*
* This specifies the default level of response detail if not set on the first matched rule or if no rules match.
*
*
* If not specified, the setting is determined via the following:
*
* {@link CallLogger#SP_responseDetail "juneau.restLogger.responseDetail"} system property.
* {@link CallLogger#SP_responseDetail "JUNEAU_RESTLOGGER_responseDetail"} environment variable.
* "STATUS_LINE" .
*
*
*
* - {@link CallLoggingDetail#STATUS_LINE STATUS_LINE} - Log only the status line.
*
- {@link CallLoggingDetail#HEADER HEADER} - Log the status line and headers.
*
- {@link CallLoggingDetail#ENTITY ENTITY} - Log the status line and headers and content if available.
*
*
* @param value
* The new value for this property, or null to use the default.
* @return This object.
*/
public Builder responseDetail(CallLoggingDetail value) {
responseDetail = value;
return this;
}
/**
* The default logging level to use for logging the request/response.
*
*
* This specifies the default logging level if not set on the first matched rule or if no rules match.
*
*
* If not specified, the setting is determined via the following:
*
* {@link CallLogger#SP_level "juneau.restLogger.level"} system property.
* {@link CallLogger#SP_level "JUNEAU_RESTLOGGER_level"} environment variable.
* "OFF" .
*
*
* @param value
* The new value for this property, or null to use the default value.
* @return This object.
*/
public Builder level(Level value) {
level = value;
return this;
}
/**
* Adds logging rules to use when debug mode is not enabled.
*
*
* Logging rules are matched in the order they are added. The first to match wins.
*
* @param values The logging rules to add to the list of rules.
* @return This object.
*/
public Builder normalRules(CallLoggerRule...values) {
for (CallLoggerRule rule : values)
normalRules.add(rule);
return this;
}
/**
* Adds logging rules to use when debug mode is enabled.
*
*
* Logging rules are matched in the order they are added. The first to match wins.
*
* @param values The logging rules to add to the list of rules.
* @return This object.
*/
public Builder debugRules(CallLoggerRule...values) {
for (CallLoggerRule rule : values)
debugRules.add(rule);
return this;
}
/**
* Shortcut for adding the same rules as normal and debug rules.
*
*
* Logging rules are matched in the order they are added. The first to match wins.
*
* @param values The logging rules to add to the list of rules.
* @return This object.
*/
public Builder rules(CallLoggerRule...values) {
return normalRules(values).debugRules(values);
}
/**
* Instantiates a new call logger based on the settings in this builder.
*
* @return A new call logger.
*/
public CallLogger build() {
return new CallLogger(this);
}
//-----------------------------------------------------------------------------------------------------------------
// Fluent setters
//-----------------------------------------------------------------------------------------------------------------
//
//
}
//-----------------------------------------------------------------------------------------------------------------
// Instance
//-----------------------------------------------------------------------------------------------------------------
private final Logger logger;
private final ThrownStore thrownStore;
private final CallLoggerRule[] normalRules, debugRules;
private final Enablement enabled;
private final Predicate enabledTest;
private final Level level;
private final CallLoggingDetail requestDetail, responseDetail;
/**
* Constructor.
*
* Subclasses typically override the {@link #init(BeanStore)} method when using this constructor.
*
* @param beanStore The bean store containing injectable beans for this logger.
*/
public CallLogger(BeanStore beanStore) {
Builder builder = init(beanStore);
this.logger = builder.logger;
this.thrownStore = builder.thrownStore;
this.normalRules = builder.normalRules.toArray(new CallLoggerRule[builder.normalRules.size()]);
this.debugRules = builder.debugRules.toArray(new CallLoggerRule[builder.debugRules.size()]);
this.enabled = builder.enabled;
this.enabledTest = builder.enabledTest;
this.requestDetail = builder.requestDetail;
this.responseDetail = builder.responseDetail;
this.level = builder.level;
}
/**
* Constructor.
*
* @param builder The builder for this logger.
*/
public CallLogger(Builder builder) {
this.logger = builder.logger;
this.thrownStore = builder.thrownStore;
this.normalRules = builder.normalRules.toArray(new CallLoggerRule[builder.normalRules.size()]);
this.debugRules = builder.debugRules.toArray(new CallLoggerRule[builder.debugRules.size()]);
this.enabled = builder.enabled;
this.enabledTest = builder.enabledTest;
this.requestDetail = builder.requestDetail;
this.responseDetail = builder.responseDetail;
this.level = builder.level;
}
/**
* Initializer.
*
* Subclasses should override this method to make modifications to the builder used to create this logger.
*
* @param beanStore The bean store containing injectable beans for this logger.
* @return A new builder object.
*/
protected Builder init(BeanStore beanStore) {
return new Builder(beanStore)
.logger(beanStore.getBean(Logger.class).orElse(null))
.thrownStore(beanStore.getBean(ThrownStore.class).orElse(null));
}
/**
* 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) {
CallLoggerRule 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);
CallLoggingDetail reqd = firstNonNull(rule.getRequestDetail(), requestDetail);
CallLoggingDetail resd = firstNonNull(rule.getResponseDetail(), responseDetail);
String method = req.getMethod();
int status = res.getStatus();
String uri = req.getRequestURI();
byte[] reqContent = getRequestContent(req);
byte[] resContent = getResponseContent(req, res);
StringBuilder sb = new StringBuilder();
if (reqd != STATUS_LINE || resd != STATUS_LINE)
sb.append("\n=== HTTP Call (incoming) ======================================================\n");
ThrownStats sti = getThrownStats(e);
sb.append('[').append(status);
if (sti != null) {
int count = sti.getCount();
sb.append(',').append(StringUtils.toHex8(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 (reqContent != null && reqd.isOneOf(HEADER ,ENTITY))
sb.append("\n\tRequest length: ").append(reqContent.length).append(" bytes");
if (resd.isOneOf(HEADER, ENTITY))
sb.append("\n\tResponse code: ").append(status);
if (resContent != null && resd.isOneOf(HEADER, ENTITY))
sb.append("\n\tResponse length: ").append(resContent.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 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 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 (reqContent != null && reqContent.length > 0 && reqd == ENTITY) {
try {
sb.append("\n---Request Content UTF-8---");
sb.append("\n").append(new String(reqContent, IOUtils.UTF8));
sb.append("\n---Request Content Hex---");
sb.append("\n").append(toSpacedHex(reqContent));
} catch (Exception e1) {
sb.append("\n").append(e1.getLocalizedMessage());
}
}
if (resContent != null && resContent.length > 0 && resd == ENTITY) {
try {
sb.append("\n---Response Content UTF-8---");
sb.append("\n").append(new String(resContent, IOUtils.UTF8));
sb.append("\n---Response Content Hex---");
sb.append("\n").append(toSpacedHex(resContent));
} catch (Exception e1) {
sb.append(e1.getLocalizedMessage());
}
}
sb.append("\n=== END ======================================================================");
}
log(level, sb.toString(), e);
}
/**
* Given the specified servlet request/response, find the rule that applies to it.
*
*
* 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 null .
*/
protected CallLoggerRule getRule(HttpServletRequest req, HttpServletResponse res) {
for (CallLoggerRule r : isDebug(req) ? debugRules : normalRules)
if (r.matches(req, res))
return r;
return DEFAULT_RULE;
}
/**
* Returns true if debug is enabled on this request.
*
*
* Looks for the request attribute "Debug" to determine whether debug is enabled.
*
*
* 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 true if debug is enabled on this request.
* @see org.apache.juneau.rest.RestContext.Builder#debugEnablement()
* @see Rest#debug()
* @see RestOp#debug()
*/
protected boolean isDebug(HttpServletRequest req) {
return firstNonNull(castOrNull(req.getAttribute("Debug"), Boolean.class), false);
}
/**
* Returns true if logging is enabled for this request.
*
*
* Uses the enabled and enabled-test settings on the matched rule and this logger to determine whether a REST
* call should be logged.
*
*
* 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 null .
* @param req The HTTP request.
* @return true if logging is enabled for this request.
*/
protected boolean isEnabled(CallLoggerRule rule, HttpServletRequest req) {
Enablement enabled = firstNonNull(rule.getEnabled(), this.enabled);
Predicate enabledTest = firstNonNull(rule.getEnabledTest(), this.enabledTest);
return enabled.isEnabled(enabledTest.test(req));
}
//-----------------------------------------------------------------------------------------------------------------
// Other methods
//-----------------------------------------------------------------------------------------------------------------
/**
* Returns the logger to use for logging REST calls.
*
*
* Returns the logger specified in the builder, or {@link Logger#getGlobal()} if it wasn't specified.
*
*
* 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.
*
*
* 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[] getRequestContent(HttpServletRequest req) {
if (req instanceof CachingHttpServletRequest)
return ((CachingHttpServletRequest)req).getContent();
return castOrNull(req.getAttribute("RequestContent"), byte[].class);
}
private byte[] getResponseContent(HttpServletRequest req, HttpServletResponse res) {
if (res instanceof CachingHttpServletResponse)
return ((CachingHttpServletResponse)res).getContent();
return castOrNull(req.getAttribute("ResponseContent"), byte[].class);
}
private ThrownStats getThrownStats(Throwable e) {
if (e == null || thrownStore == null)
return null;
return thrownStore.getStats(e).orElse(null);
}
@Override /* Object */
public String toString() {
return filteredMap()
.append("logger", logger)
.append("thrownStore", thrownStore)
.append("enabled", enabled)
.append("level", level)
.append("requestDetail", requestDetail)
.append("responseDetail", responseDetail)
.append("normalRules", normalRules.length == 0 ? null : normalRules)
.append("debugRules", debugRules.length == 0 ? null : debugRules)
.asReadableString();
}
}