Skip to content

Instrumenting Servlets

Karsten Schnitter edited this page Mar 1, 2023 · 12 revisions

Servlet instrumentation enables application developers to enhance log messages with metadata extracted from HTTP headers and generates request logs. It is implemented as a standard servlet filter as defined in the Java Servlet Specification, Version 3.0 . There are several distinct feature, that can be configured separately.

We provide two versions of the servlet instrumentation to address the packaging switch from javax.servlet to jakarta.servlet:

  • cf-java-logging-support-servlet using javax.servlet in version 3.1
  • cf-java-logging-support-servlet-jakarta usin jakarta.servlet in version 5.0

Both artefacts contain the same classes and provide the same features. Depending on your project setup, you can choose the matching dependency version. For example, when building Spring Boot 3.0 apps, choose the Jakarta version.

Quick Start Guide

Request instrumentation is configured by adding the RequestLoggingFilter to your servlet configuration. This filter contains the full feature set and is a good general approach for most applications. Custom configurations are described in section Customizing Request Filtering. You only need to take two steps:

Adding the Maven Dependency

Add either of the following Maven dependencies to your pom.xml. It contains all required classes to provide request instrumentation for your application.

For javax.servlet support (Java 8 and before):

<!-- We're using the Servlet Filter instrumentation -->
<dependency>
   <groupId>com.sap.hcp.cf.logging</groupId>
   <artifactId>cf-java-logging-support-servlet</artifactId>
   <version>${cf-logging-version}</version>
</dependency>

For jakarta.servlet support (Java 11 and beyond):

<!-- We're using the Servlet Filter instrumentation -->
<dependency>
   <groupId>com.sap.hcp.cf.logging</groupId>
   <artifactId>cf-java-logging-support-servlet-jakarta</artifactId>
   <version>${cf-logging-version}</version>
</dependency>

Either configure a property cf-logging-version with the current library version or just put it in the <version> tag.

Registering the Servlet Filter

The servlet filter is enabled by registering com.sap.hcp.cf.logging.servlet.filter.RequestLoggingFilter as filter in a servlet context. It should be registered for the dispatch type request to function properly. See the examples below for more details.

Using web.xml

The easiest way to enable that servlet filter is to declare it in your application's web.xml file. To do so, add the following lines to that file, as we've done in the sample application:

<filter>
   <filter-name>request-logging</filter-name>
   <filter-class>com.sap.hcp.cf.logging.servlet.filter.RequestLoggingFilter</filter-class>
</filter>
<filter-mapping>
   <filter-name>request-logging</filter-name>
   <url-pattern>/*</url-pattern>
</filter-mapping>

FilterRegistrationBean in Spring (Boot)

You can use FilterRegistrationBean to enable the servlet filter in Spring Boot. Add the following bean to your @Configuration file:

@Bean
public FilterRegistrationBean loggingFilter() {
	FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
	filterRegistrationBean.setFilter(new RequestLoggingFilter());
	filterRegistrationBean.setName("request-logging");
	filterRegistrationBean.addUrlPatterns("/*");
	filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST);
	return filterRegistrationBean;
}

If you use Spring Security, the above registration will register the RequestLoggingFilter after the Spring Security filter. So request instrumentation will only happen after a request is accepted. If you want to have request intrumentation for all requests, add the following line to raise the order in the FilterRegistrationBean:

   filterRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);

Customizing Request Filtering

The RequestLoggingFilter bundles different functionalities in one easy to use filter. There are use-cases, where some of the features may not be required but some are. For those scenarios, all features are provided by separate filters, that can be configured to match the requirements of the developer.

The following table gives an overview of the available feature and the respective filters.

Feature Filter Description
VCAP metadata com.sap.hcp.cf.logging.servlet.filter.AddVcapEnvironmentToLogContextFilter Extracts metadata about the application from the VCAP environment variable and adds them to the LogContext.
HTTP header propagation com.sap.hcp.cf.logging.servlet.filter.AddHttpHeadersToLogContextFilter Extracts HTTP headers and adds them to the LogContext.
Correlation Id com.sap.hcp.cf.logging.servlet.filter.CorrelationIdFilter Extracts a correlation id from an HTTP header or generates one, if none is found. Adds the correlation id to the LogContext.
Dynamic Log Levels com.sap.hcp.cf.logging.servlet.filter.DynamicLogLevelFilter Enables dynamic log level switching with JWT tokens.
Request Logs com.sap.hcp.cf.logging.servlet.filter.GenerateRequestLogFilter Creates the request logs for incoming requests.
LogContext to Request Attribute com.sap.hcp.cf.logging.servlet.filter.LogContextToRequestAttributeFilter Adds the current LogContext as an request attribute for asynchronous request handling. This feature is also part of generating reqeust logs.

The order of that table corresponds to the ordering these features are applied in the RequestLoggingFilter. Most filters add fields to the LogContext, which leads to emitting these fields as part of the JSON log messages generated during request handling. By customizing the filter registration, you can choose what filters to apply on which parts of your application explicitly. To ease extensions, the static method RequestLoggingFilter.getDefaultFilters()returns a freshly generated array of filter instances in the order used by the RequestLoggingFilter.

Features and Filters

This section describes the provided features. It also explains possible configuration options and extension endpoints.

VCAP Metadata

Applications running in CloudFoundry can access environment variables, that are configured in the manifest or injected by the runtime, e.g. service bindings. To ease identification of the log message source the following information is read from the VCAP_APPLICATION environment variable:

  • application id
  • application name
  • instance index
  • space id
  • space name
  • organization id
  • organization name

For details see VcapEnvReader.

The extracted information can usually also be obtained from CF Loggregator shipment channels. When your logging system has access to those, you can omit this feature.

VCAP metadata is provided by com.sap.hcp.cf.logging.servlet.filter.AddVcapEnvironmentToLogContextFilter. The filter does not provide custom configuration or extensibility. Basic web.xml configuration is as follows:

<filter>
   <filter-name>log-vcap-metadata</filter-name>
   <filter-class>com.sap.hcp.cf.logging.servlet.filter.AddVcapEnvironmentToLogContextFilter</filter-class>
</filter>
<filter-mapping>
   <filter-name>log-vcap-metadata</filter-name>
   <url-pattern>/*</url-pattern>
</filter-mapping>

HTTP Header Propagation

HTTP headers can serve as a powerful tool to group log messages, e.g. request or tenant ids. This helps in log analysis to correlate different messages. The AddHttpHeadersToLogContextFilter by default extracts the following HTTP headers as key-value pairs and adds them to the LogContext:

  • x-vcap-request-id
  • X-CorrelationID
  • tenantid
  • sap-passport

Note: The correlation and request id are also processed by the CorrelationIdFilter. See this section for details on the differences between the two filters.

Default HTTP Header Propagation

To use the default HTTP headers as described above, simply register the AddHttpHeadersToLogContextFilter without parameters:

<filter>
   <filter-name>log-http-headers</filter-name>
   <filter-class>com.sap.hcp.cf.logging.servlet.filter.AddHttpHeadersToLogContextFilter</filter-class>
</filter>
<filter-mapping>
   <filter-name>log-http-headers</filter-name>
   <url-pattern>/*</url-pattern>
</filter-mapping>
Custom HTTP Header Propagation

The AddHttpHeadersToLogContextFilter comes with three constructors:

    public AddHttpHeadersToLogContextFilter() {
        this(HttpHeaders.propagated());
    }

    public AddHttpHeadersToLogContextFilter(HttpHeader... headers) {
        this(Collections.emptyList(), headers);
    }

    public AddHttpHeadersToLogContextFilter(List<? extends HttpHeader> list, HttpHeader... custom) {
        Stream<HttpHeader> allHeaders = Stream.concat(list.stream(), Arrays.stream(custom));
        this.headers = unmodifiableList(allHeaders.filter(HttpHeader::isPropagated).collect(toList()));
        this.fields = unmodifiableList(headers.stream().map(HttpHeader::getField).filter(Objects::nonNull).collect(
                                                                                                                   toList()));
    }

The first constructor is used in the default configuration. The second and third constructor allow to exchange and extended the list of forwarded headers. If you can register objects as filter, e.g. using Spring Boot, just use these constructors with the headers you need. If you can only register classes, e.g. using web.xml, create a custom subclass of AddHttpHeadersToLogContextFilter and redirect the default constructor to one of the other two constructors.

To provide custom headers, you have to implement HttpHeader:

public interface HttpHeader {

	boolean isPropagated(); // needs to be true for this use-case

	String getName();  // the name of the HTTP header

	String getField(); // the name of the JSON log field

	List<HttpHeader> getAliases(); // other headers to use, if this one is missing

	String getFieldValue(); // get current or default value from LogContext
}

The default headers are defined in HttpHeaders.

Custom Header Example: W3C Trace State

The W3C Trace Context defines two HTTP headers for tracing requests: traceparent and tracestate. traceparent is already support by default. You can add support for tracestate with the following implementation:

public class W3cTraceState implements HttpHeader {

    @Override
    public boolean isPropagated() {
        return true;
    }

    @Override
    public String getName() {
        return "tracestate";
    }

    @Override
    public String getField() {
        return "w3c_tracestate";
    }

    @Override
    public List<HttpHeader> getAliases() {
        return Collections.emptyList();
    }

    @Override
    public String getFieldValue() {
        return LogContext.get(getField());
    }
}

This implementation can now be used in the AddHttpHeadersToLogContextFilter:

Filter extendedHeaderFilter = new AddHttpHeadersToLogContextFilter(HttpHeaders.propagated(), new W3cTraceState());

This adds the new header to the default headers provided by the library.

Correlation Id

Correlation ids are a mechanism to correlate multiple log messages to one request. They are taken from an HTTP header and should be forwarded to all outgoing requests made to serve the incoming request. This allows tracing the request execution amongst different applications. The CorrelationIdFilter implements this simple tracing approach.

By default it uses the header X-CorrelationID with a fall-back to x-vcap-request-id, if X-CorrelationID cannot be found. If neither header is present the filter checks the LogContext for the field correlation_id. If there is still no value to be found a random correlation-id is generated. In all cases, the correlation id is added as response header X-CorrelationID, if possible. The correlation id is added as field correlation_id to the LogContext.

The main additional features compared to the AddHttpHeadersToLogContextFilter are:

  • generation of a random correlation id, if none found
  • addition of response header X-CorrelationID

Both filters produce idempotent results and can be combined in any order.

Default Correlation Id

To use the default HTTP headers as described above, simply register the [CorrelationIdFilter:

<filter>
   <filter-name>log-correlation-id</filter-name>
   <filter-class>com.sap.hcp.cf.logging.servlet.filter.CorrelationIdFilter</filter-class>
</filter>
<filter-mapping>
   <filter-name>log-correlation-id</filter-name>
   <url-pattern>/*</url-pattern>
</filter-mapping>
Custom Correlation Id

The CorrelationIdFilter has a constructor, that takes an HttpHeader to be used as correlation-id as an argument. If you can register objects as filter, e.g. using Spring Boot, just use this constructor with the headers you need. If you can only register classes, e.g. using web.xml, create a custom subclass of CorrelationIdFilter, that calls this constructor with the header you want. This is very similar to propagating custom http headers with the AddHttpHeadersToLogContextFilter.

Dynamic Log Levels

Dynamic Log Levels refer to the possibility to switch the configured log levels at runtime. There are different approaches to achieve that goal either through configuration servers or exposing a management endpoint, e.g. in Spring Boot Actuator. This library takes a different approach, that is described in detail in the Dynamic Log Levels documentation. In summary, there is the DynamicLogLevelFilter, that scans HTTP requests for a specific header containing a JWT. After validation the token is inspected for claims changing the logging configuration. The changed configuration is applied by a filter class depending on the used logging backend (log4j2 or logback). It is possible to customize the JWT processing and configuration such as the HTTP header to be used. See DynamicLogLevelFilter for details.

To use the default Dynamic Log Level implementation, simply register the [DynamicLogLevelFilter:

<filter>
   <filter-name>dynamic-log-levels</filter-name>
   <filter-class>com.sap.hcp.cf.logging.servlet.filter.DynamicLogLevelFilter</filter-class>
</filter>
<filter-mapping>
   <filter-name>dynamic-log-levels</filter-name>
   <url-pattern>/*</url-pattern>
</filter-mapping>

Since the JWT processing requires the additional dependency java-jwt, there is a simplified version of the RequestLoggingFilter, that is stripped of the dynamic log level feature. To use this simplified version, register the [StaticLevelRequestLoggingFilter:

<filter>
   <filter-name>static-request-logging</filter-name>
   <filter-class>com.sap.hcp.cf.logging.servlet.filter.StaticLevelRequestLoggingFilter</filter-class>
</filter>
<filter-mapping>
   <filter-name>static-request-logging</filter-name>
   <url-pattern>/*</url-pattern>
</filter-mapping>

You can exclude the java-jwt dependency when using the StaticLevelRequestLoggingFilter:

<dependency>
   <groupId>com.sap.hcp.cf.logging</groupId>
   <artifactId>cf-java-logging-support-servlet</artifactId>
   <exclusions>
      <exclusion>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
      </exclusion>
   </exclusions>
</dependency>

Request Logs

The main goal of the request instrumentation is to provide request logs. This means, for every incoming request a log message containing information about request and response is emitted. These messages contain metrics similar to Apache access logs. The main benefits of creating these logs in the library are:

  • metrics in particular response times are determined for the application and do not include network hops due to routing
  • request logs can be customized by the other features, such as correlation ids and custom headers
  • the logs are generated regardless of the application deployment, e.g. they do not rely on the CF Gorouter

Request logs are emitted in JSON format. See the exported fields for request metrics for details.

To generate request logs, register the GenerateRequestLogFilter:

<filter>
   <filter-name>generate-request-logs</filter-name>
   <filter-class>com.sap.hcp.cf.logging.servlet.filter.GenerateRequestLogFilter</filter-class>
</filter>
<filter-mapping>
   <filter-name>generate-request-logs</filter-name>
   <url-pattern>/*</url-pattern>
</filter-mapping>
Configuration Details of Request Logs

Note: All configuration describe in the following sections also apply to the general RequestLoggingFilter and StaticLevelRequestLoggingFilter.

There are several ways to customize the generated request logs. They are configured by different mechanisms. The following table shows the properties and the kind of configuration.

Property Configuration Parameter
generate Logs logging configuration for RequestLogger
wrap request filter init parameter "wrapRequest"
wrap response filter init parameter "wrapResponse"
include sensitive data environment variable "LOG_SENSITIVE_CONNECTION_DATA"
include remote user environment variable "LOG_REMOTE_USER"
include referrer environment variable "LOG_REFERER"
include x-ssl-headers environment variable "LOG_SSL_HEADERS"
Disabling Generation of Request Logs

Request logs are generated by the RequestLogger with an SL4J marker REQUEST_MARKER at INFO level. You can disable the generation of request logs by disabling these logs in you logging configuration.

Using Log4j2 add the following line:

<Logger name="com.sap.hcp.cf.logging.servlet.filter.RequestLogger" level="WARN" />

Using Logback add the following line:

<logger name="com.sap.hcp.cf.logging.servlet.filter.RequestLogger"  level="WARN" />

This configuration is helpful, when combined with dynamic log levels. It can be used to dynamically control the generation of request logs by a JWT header. If the request logs are disabled no instrumentation of requests or responses are done.

Filter Parameters

The GenerateRequestLogFilter wraps HTTP requests and responses to determine there sizes. This wrapping can be disabled by the filter parameters wrapRequest and wrapResponse respectively.

<filter>
   <filter-name>generate-request-logs</filter-name>
   <filter-class>com.sap.hcp.cf.logging.servlet.filter.GenerateRequestLogFilter</filter-class>
   <init-param>
      <param-name>wrapRequest</param-name>
      <param-value>false</param-value>
   </init-param>
   <init-param>
      <param-name>wrapResponse</param-name>
      <param-value>false</param-value>
   </init-param>
</filter>
<filter-mapping>
   <filter-name>generate-request-logs</filter-name>
   <url-pattern>/*</url-pattern>
</filter-mapping>

Note: In order to support asynchronous request handling, HTTP request will always be wrapped by LoggingContextRequestWrapper. This can only be disabled by disabling request logs.

Inclusion of Sensitive Data

By default the GenerateRequestLogFilter will not log personalizable information on the incoming requests. This is controlled by three environment variables: LOG_SENSITIVE_CONNECTION_DATA, LOG_REMOTE_USER and LOG_REFERER. You need to provide the value true for each of those variables to include the respective information in the request logs. The following table shows what data can be enabled by which variable.

Environment Variable Provided Request Data
LOG_SENSITIVE_CONNECTION_DATA remote address
remote host from request
remote port
HTTP header x-forwarded-for
LOG_REMOTE_USER remote user
LOG_REFERER HTTP header referer

Note: Due to Load-Balancing in CF, the remote data may refer to the Gorouter and not the user client.

Inclusion of SSL Verification Headers

HA-Proxy can be configured to terminate ssl connections and forward the verification as http headers to the application. See the HA-Proxy Documentation for details. This results in eight possible headers attached to the http request:

  • x-ssl
  • x-ssl-client-verify
  • x-ssl-client-subject-dn
  • x-ssl-client-subject-cn
  • x-ssl-client-issuer-dn
  • x-ssl-client-notbefore
  • x-ssl-client-notafter
  • x-ssl-client-session-id

These headers might be logged as fields within the request logs with hyphens replaced by underscores. In any case, these fields are only emitted, when explicitly enabled. This is achieved by providing the environment variable LOG_SSL_HEADERS with a value of true.

LogContext to Request Attribute

The Java Servlet API 3.0 introduced asynchronous request handling. The main distinction of asynchronous request handling is, that requests are no longer handled by the same Java thread in which was assigned on request dispatch. Logging usually leverages a context (MDC) bound to the current thread to manage metadata. #cf-java-logging-support uses the MDC for request tracking such as request id and further CF metadata. When changing threads this context may be lost. Our current implementation ensures that the context is properly migrated within the threads of the servlet container.

To enable custom thread models or asynchronous execution the GenerateRequestLogFilter will add the MDC context map as a request attribute with the key org.slf4j.MDC. If necessary you can set the MDC in any custom thread by calling

MDC.setContextMap(httpRequest.getAttribute(MDC.class.getName()));

Note: This is only necessary when you create own threads, e.g. using custom Executors. One such case would be the @Async support in Spring.

If you do not want to use the GenerateRequestLogFilter but still want to have the LogContext added as request attribute, you can configure the LogContextToRequestAttributeFilter:

<filter>
   <filter-name>mdc-request-attribute</filter-name>
   <filter-class>com.sap.hcp.cf.logging.servlet.filter.LogContextToRequestAttributeFilter</filter-class>
</filter>
<filter-mapping>
   <filter-name>mdc-request-attribute</filter-name>
   <url-pattern>/*</url-pattern>
</filter-mapping>

Note: This functionality is contained in the GenerateRequestLogFilter and hence in both in the RequestLoggingFilter and the StaticLevelRequestLoggingFilter. You will only need the LogContextToRequestAttributeFilter if you use neither of those three filters.