All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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} *
*/ 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: *

    *
  1. {@link CallLogger#SP_logger "juneau.restLogger.logger"} system property. *
  2. {@link CallLogger#SP_logger "JUNEAU_RESTLOGGER_LOGGER"} environment variable. *
  3. "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: *

    *
  1. {@link CallLogger#SP_logger "juneau.restLogger.logger"} system property. *
  2. {@link CallLogger#SP_logger "JUNEAU_RESTLOGGER_LOGGER"} environment variable. *
  3. "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(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy