Skip to content

Commit

Permalink
Add an example of customization.
Browse files Browse the repository at this point in the history
  • Loading branch information
Chavjoh committed Jan 20, 2024
1 parent ee297dd commit 4cc3092
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 23 deletions.
12 changes: 12 additions & 0 deletions README.md
Expand Up @@ -93,6 +93,18 @@ with the following MDC fields:
* `response-body: { "id" : 1, "content": "Something" }`
* `duration: 15`

## Extension

An example of extension of the filter is available
with [UserLogged](src%2Ftest%2Fjava%2Fcom%2Fchavaillaz%2Fjakarta%2Frs%2FUserLogged.java)
and [UserLoggedFilter](src%2Ftest%2Fjava%2Fcom%2Fchavaillaz%2Fjakarta%2Frs%2FUserLoggedFilter.java).
In this example, you can find the following customization of the original filter:

* Log new **user-id** field in MDC
* Log new **user-agent** field in MDC if activated in annotation
* Change **request-id** logic to get it from a header field
* Rename MDC field of **request-id** to **request-identifier**

## Contributing

If you have a feature request or found a bug, you can:
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/chavaillaz/jakarta/rs/Logged.java
Expand Up @@ -21,14 +21,14 @@
public @interface Logged {

/**
* Indicates if the request body must be as MDC field when logging the request.
* Indicates if the request body must be as MDC field when processing and logging the request.
*
* @return {@code true} to log the request body, {@code false} otherwise
*/
boolean requestBody() default false;

/**
* Indicates if the response body must be as MDC field when logging the request.
* Indicates if the response body must be as MDC field when processing and logging the request.
*
* @return {@code true} to log the response body, {@code false} otherwise
*/
Expand Down
31 changes: 31 additions & 0 deletions src/test/java/com/chavaillaz/jakarta/rs/AbstractFilterTest.java
@@ -0,0 +1,31 @@
package com.chavaillaz.jakarta.rs;

import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.slf4j.MDC;

public abstract class AbstractFilterTest {

protected static final InMemoryAppender LIST_APPENDER = InMemoryAppender.createDefaultAppender();

@BeforeAll
static void registerListAppender() {
LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);
Configuration configuration = loggerContext.getConfiguration();
LoggerConfig rootLoggerConfig = configuration.getLoggerConfig("");
rootLoggerConfig.addAppender(LIST_APPENDER, Level.ALL, null);
}

@BeforeEach
void setupTest() {
MDC.clear();
LIST_APPENDER.getMessages().clear();
LIST_APPENDER.start();
}

}
25 changes: 4 additions & 21 deletions src/test/java/com/chavaillaz/jakarta/rs/LoggedFilterTest.java
Expand Up @@ -23,21 +23,14 @@
import jakarta.ws.rs.container.ResourceInfo;
import jakarta.ws.rs.ext.Providers;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.jboss.resteasy.core.Headers;
import org.jboss.resteasy.core.interception.jaxrs.ContainerResponseContextImpl;
import org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.jboss.resteasy.plugins.providers.DefaultTextPlain;
import org.jboss.resteasy.specimpl.BuiltResponse;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
Expand All @@ -47,10 +40,10 @@
import org.slf4j.MDC;

@ExtendWith(MockitoExtension.class)
@DisplayName("Original filter")
@Logged(requestBody = true, responseBody = true)
class LoggedFilterTest {
class LoggedFilterTest extends AbstractFilterTest {

private static final InMemoryAppender LIST_APPENDER = InMemoryAppender.createDefaultAppender();
private static final String INPUT = "input";
private static final String OUTPUT = "output";

Expand All @@ -63,19 +56,9 @@ class LoggedFilterTest {
@InjectMocks
LoggedFilter requestLoggingFilter;

@BeforeAll
static void registerListAppender() {
LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);
Configuration configuration = loggerContext.getConfiguration();
LoggerConfig rootLoggerConfig = configuration.getLoggerConfig("");
rootLoggerConfig.addAppender(LIST_APPENDER, Level.ALL, null);
}

@BeforeEach
@Override
void setupTest() {
MDC.clear();
LIST_APPENDER.getMessages().clear();
LIST_APPENDER.start();
super.setupTest();
// Returns this method and class as the resource matched by the queries in tests
doReturn(new Object() {}.getClass().getEnclosingMethod()).when(resourceInfo).getResourceMethod();
doReturn(this.getClass()).when(resourceInfo).getResourceClass();
Expand Down
37 changes: 37 additions & 0 deletions src/test/java/com/chavaillaz/jakarta/rs/UserLogged.java
@@ -0,0 +1,37 @@
package com.chavaillaz.jakarta.rs;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import jakarta.ws.rs.NameBinding;

/**
* Annotation activating the filter {@link UserLoggedFilter}
* in order to log incoming requests received by a JAX-RS resource with user specific data.
*/
@Documented
@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface UserLogged {

/**
* Base filter logging configuration for {@link LoggedFilter}
*
* @return The annotation configuration
*/
Logged logging() default @Logged();

/**
* Indicates if the user agent must be as MDC field when processing and logging the request.
*
* @return {@code true} to log the user agent, {@code false} otherwise
*/
boolean userAgent() default false;

}
61 changes: 61 additions & 0 deletions src/test/java/com/chavaillaz/jakarta/rs/UserLoggedFilter.java
@@ -0,0 +1,61 @@
package com.chavaillaz.jakarta.rs;

import static com.chavaillaz.jakarta.rs.LoggedField.REQUEST_ID;

import java.util.Optional;

import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.ext.Provider;
import org.slf4j.MDC;

@UserLogged
@Provider
public class UserLoggedFilter extends LoggedFilter {

protected static final String REQUEST_IDENTIFIER = "request-identifier";
protected static final String USER_ID = "user-id";
protected static final String USER_AGENT = "user-agent";

public UserLoggedFilter() {
// Add new MDC fields to be finally cleaned up
this.mdcFields.put(USER_ID, USER_ID);
this.mdcFields.put(USER_AGENT, USER_AGENT);

// Edit MDC field name when needed, for example to be aligned between applications
// or follow schemas defined for Kibana, OpenSearch, Splunk
this.mdcFields.put(REQUEST_ID.name(), REQUEST_IDENTIFIER);
}

@Override
protected Optional<Logged> getAnnotation() {
// Need to give the base configuration to the parent
return getAnnotation(resourceInfo, UserLogged.class)
.map(UserLogged::logging);
}

@Override
protected String getRequestId(ContainerRequestContext requestContext) {
// Take the request identifier from custom header received
return requestContext.getHeaderString("X-Case-ID");
}

@Override
public void filter(ContainerRequestContext requestContext) {
super.filter(requestContext);

// Add the user currently logged in, possibly by querying injected entity
MDC.put(USER_ID, "Doe");

// Log specific field if activated in the new annotation
logUserAgent(requestContext);
}

private void logUserAgent(ContainerRequestContext requestContext) {
getAnnotation(resourceInfo, UserLogged.class)
.map(UserLogged::userAgent)
.filter(loggingActivated -> loggingActivated)
.map(logging -> requestContext.getHeaderString("User-Agent"))
.ifPresent(origin -> MDC.put(USER_AGENT, origin));
}

}
69 changes: 69 additions & 0 deletions src/test/java/com/chavaillaz/jakarta/rs/UserLoggedFilterTest.java
@@ -0,0 +1,69 @@
package com.chavaillaz.jakarta.rs;

import static com.chavaillaz.jakarta.rs.UserLoggedFilter.REQUEST_IDENTIFIER;
import static com.chavaillaz.jakarta.rs.UserLoggedFilter.USER_AGENT;
import static com.chavaillaz.jakarta.rs.UserLoggedFilter.USER_ID;
import static jakarta.ws.rs.core.MediaType.TEXT_PLAIN_TYPE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.doReturn;

import java.net.URISyntaxException;

import jakarta.ws.rs.container.ResourceInfo;
import jakarta.ws.rs.ext.Providers;
import org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.MDC;

@ExtendWith(MockitoExtension.class)
@DisplayName("Custom filter")
@UserLogged(logging = @Logged(requestBody = true, responseBody = true), userAgent = true)
class UserLoggedFilterTest extends AbstractFilterTest {

@Mock
ResourceInfo resourceInfo;

@Mock
Providers providers;

@InjectMocks
UserLoggedFilter requestLoggingFilter;

@Override
void setupTest() {
super.setupTest();
// Returns this method and class as the resource matched by the queries in tests
doReturn(new Object() {}.getClass().getEnclosingMethod()).when(resourceInfo).getResourceMethod();
doReturn(this.getClass()).when(resourceInfo).getResourceClass();
}

@Test
@DisplayName("Check filter on request sets up MDC fields")
void filterRequestCheckMdc() throws URISyntaxException {
// Given
PreMatchContainerRequestContext requestContext = getRequestContext();

// When
requestLoggingFilter.filter(requestContext);

// Then
assertEquals("CaseId", MDC.get(REQUEST_IDENTIFIER));
assertEquals("Doe", MDC.get(USER_ID));
assertEquals("Opera", MDC.get(USER_AGENT));
}

private PreMatchContainerRequestContext getRequestContext() throws URISyntaxException {
return new PreMatchContainerRequestContext(
MockHttpRequest.create("POST", "example.company.com/service")
.header("X-Case-ID", "CaseId")
.header("User-Agent", "Opera")
.contentType(TEXT_PLAIN_TYPE));
}

}

0 comments on commit 4cc3092

Please sign in to comment.