Skip to content

Commit 42f8aa3

Browse files
Add Filter to Extract Http Headers and Write them as Log Fields
1 parent 5607509 commit 42f8aa3

File tree

5 files changed

+277
-63
lines changed

5 files changed

+277
-63
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.sap.hcp.cf.logging.servlet.filter;
2+
3+
import static java.util.Collections.unmodifiableList;
4+
import static java.util.stream.Collectors.toList;
5+
6+
import java.util.Arrays;
7+
import java.util.Collections;
8+
import java.util.List;
9+
import java.util.stream.Stream;
10+
11+
import javax.servlet.http.HttpServletRequest;
12+
import javax.servlet.http.HttpServletResponse;
13+
14+
import com.sap.hcp.cf.logging.common.LogContext;
15+
import com.sap.hcp.cf.logging.common.request.HttpHeader;
16+
import com.sap.hcp.cf.logging.common.request.HttpHeaders;
17+
18+
/**
19+
* Extracts the HTTP headers from the request and adds them to the logging
20+
* context. It defaults to {@link HttpHeaders#propagated()}. Custom headers can
21+
* be used.
22+
*/
23+
public class AddHttpHeadersToLogContextFilter extends AbstractLoggingFilter {
24+
25+
private List<HttpHeader> headers;
26+
private List<String> fields;
27+
28+
/**
29+
* The default constructor uses {@link HttpHeaders#propagated()} to define
30+
* the HTTP headers, that are added to the logging context.
31+
*/
32+
public AddHttpHeadersToLogContextFilter() {
33+
this(HttpHeaders.propagated());
34+
}
35+
36+
public AddHttpHeadersToLogContextFilter(HttpHeader... headers) {
37+
this(Collections.emptyList(), headers);
38+
}
39+
40+
/**
41+
* Use this constructor to add your own HTTP headers to the default list.
42+
* You need to implement {@link HttpHeader}. Note, that
43+
* {@link HttpHeader#isPropagated} needs to be true. All other headers will
44+
* be ignored. Usage to add your own header would be:
45+
* {@code new AddHttpHeadersToLogContextFilter(HttpHeaders.propagated, yourHeader)}
46+
*
47+
* @param list
48+
* a list of {@link HttpHeader}, can be default
49+
* {@link HttpHeaders#propagated()}
50+
* @param custom
51+
* a single {@link HttpHeader} to add to the list
52+
*/
53+
public AddHttpHeadersToLogContextFilter(List<? extends HttpHeader> list, HttpHeader... custom) {
54+
Stream<HttpHeader> allHeaders = Stream.concat(list.stream(), Arrays.stream(custom));
55+
this.headers = unmodifiableList(allHeaders.filter(HttpHeader::isPropagated).collect(toList()));
56+
this.fields = unmodifiableList(headers.stream().map(HttpHeader::getField).filter(f -> f != null).collect(
57+
toList()));
58+
}
59+
60+
@Override
61+
protected void preProcess(HttpServletRequest request, HttpServletResponse response) {
62+
for (HttpHeader header: headers) {
63+
String headerValue = HttpHeaderUtilities.getHeaderValue(request, header);
64+
if (header.getField() != null && headerValue != null) {
65+
LogContext.add(header.getField(), headerValue);
66+
}
67+
}
68+
}
69+
70+
@Override
71+
protected void postProcess(HttpServletRequest request, HttpServletResponse response) {
72+
fields.forEach(LogContext::remove);
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package com.sap.hcp.cf.logging.servlet.filter;
2+
3+
import static org.hamcrest.MatcherAssert.assertThat;
4+
import static org.hamcrest.Matchers.any;
5+
import static org.hamcrest.Matchers.containsInAnyOrder;
6+
import static org.hamcrest.Matchers.either;
7+
import static org.hamcrest.Matchers.equalTo;
8+
import static org.hamcrest.Matchers.hasEntry;
9+
import static org.hamcrest.Matchers.is;
10+
import static org.hamcrest.Matchers.not;
11+
import static org.hamcrest.Matchers.nullValue;
12+
import static org.mockito.Mockito.doAnswer;
13+
import static org.mockito.Mockito.when;
14+
15+
import java.util.stream.Stream;
16+
17+
import javax.servlet.FilterChain;
18+
import javax.servlet.http.HttpServletRequest;
19+
import javax.servlet.http.HttpServletResponse;
20+
21+
import org.junit.Before;
22+
import org.junit.Test;
23+
import org.junit.runner.RunWith;
24+
import org.mockito.Mock;
25+
import org.mockito.runners.MockitoJUnitRunner;
26+
import org.slf4j.MDC;
27+
28+
import com.sap.hcp.cf.logging.common.request.HttpHeader;
29+
import com.sap.hcp.cf.logging.common.request.HttpHeaders;
30+
31+
@RunWith(MockitoJUnitRunner.class)
32+
public class AddHttpHeadersToLogContextFilterTest {
33+
34+
@Mock
35+
private HttpServletRequest request;
36+
@Mock
37+
private HttpServletResponse response;
38+
@Mock
39+
private FilterChain chain;
40+
41+
private ContextMapExtractor mdcExtractor;
42+
43+
@Before
44+
public void setUp() throws Exception {
45+
MDC.clear();
46+
mdcExtractor = new ContextMapExtractor();
47+
doAnswer(mdcExtractor).when(chain).doFilter(request, response);
48+
}
49+
50+
@Test
51+
public void addsSingleHttpHeader() throws Exception {
52+
when(request.getHeader("my-header")).thenReturn("my-value");
53+
HttpTestHeader myHeader = new HttpTestHeader("my-header", "my-field", null, true);
54+
55+
new AddHttpHeadersToLogContextFilter(myHeader).doFilter(request, response, chain);
56+
57+
assertThat(mdcExtractor.getField("my-field"), is(equalTo("my-value")));
58+
}
59+
60+
@Test
61+
public void ignoresNotPropagatedHttpHeader() throws Exception {
62+
when(request.getHeader("my-header")).thenReturn("my-value");
63+
HttpTestHeader myHeader = new HttpTestHeader("my-header", "my-field", null, false);
64+
65+
new AddHttpHeadersToLogContextFilter(myHeader).doFilter(request, response, chain);
66+
67+
assertThat(mdcExtractor.getContextMap(), is(nullValue()));
68+
}
69+
70+
@Test
71+
public void ignoresHttpHeadersWithoutField() throws Exception {
72+
when(request.getHeader("my-header")).thenReturn("my-value");
73+
HttpTestHeader myHeader = new HttpTestHeader("my-header", null, null, true);
74+
75+
new AddHttpHeadersToLogContextFilter(myHeader).doFilter(request, response, chain);
76+
77+
assertThat(mdcExtractor.getContextMap(), is(nullValue()));
78+
}
79+
80+
81+
@Test
82+
public void ignoresMissingHeaderValues() throws Exception {
83+
HttpTestHeader myHeader = new HttpTestHeader("my-header", "my-field", null, true);
84+
85+
new AddHttpHeadersToLogContextFilter(myHeader).doFilter(request, response, chain);
86+
87+
assertThat(mdcExtractor.getContextMap(), is(nullValue()));
88+
}
89+
90+
@Test
91+
public void removesFieldAfterFiltering() throws Exception {
92+
when(request.getHeader("my-header")).thenReturn("my-value");
93+
HttpTestHeader myHeader = new HttpTestHeader("my-header", "my-field", null, true);
94+
95+
new AddHttpHeadersToLogContextFilter(myHeader).doFilter(request, response, chain);
96+
97+
assertThat(MDC.getCopyOfContextMap(), either(not(hasEntry(any(String.class), any(String.class)))).or(is(nullValue())));
98+
}
99+
100+
@Test
101+
public void addsDefaultFields() throws Exception {
102+
streamDefaultHeaders().map(HttpHeader::getName).forEach(n -> when(request.getHeader(n)).thenReturn(n + "-test_value"));
103+
104+
new AddHttpHeadersToLogContextFilter().doFilter(request, response, chain);
105+
106+
String[] fields = streamDefaultHeaders().map(HttpHeader::getField).toArray(String[]::new);
107+
assertThat(mdcExtractor.getContextMap().keySet(), containsInAnyOrder(fields));
108+
}
109+
110+
private Stream<HttpHeaders> streamDefaultHeaders() {
111+
return HttpHeaders.propagated().stream();
112+
}
113+
114+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.sap.hcp.cf.logging.servlet.filter;
2+
3+
import java.util.Map;
4+
5+
import org.mockito.invocation.InvocationOnMock;
6+
import org.mockito.stubbing.Answer;
7+
import org.slf4j.MDC;
8+
9+
public class ContextMapExtractor implements Answer<Void> {
10+
11+
private Map<String, String> contextMap;
12+
13+
public Map<String, String> getContextMap() {
14+
return contextMap;
15+
}
16+
17+
public String getField(String name) {
18+
return contextMap.get(name);
19+
}
20+
21+
@Override
22+
public Void answer(InvocationOnMock invocation) throws Throwable {
23+
contextMap = MDC.getCopyOfContextMap();
24+
return null;
25+
}
26+
}

cf-java-logging-support-servlet/src/test/java/com/sap/hcp/cf/logging/servlet/filter/CorrelationIdFilterTest.java

Lines changed: 14 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
import static org.mockito.Mockito.verifyNoMoreInteractions;
1111
import static org.mockito.Mockito.when;
1212

13-
import java.util.Collections;
14-
import java.util.List;
1513
import java.util.UUID;
1614

1715
import javax.servlet.FilterChain;
@@ -22,9 +20,7 @@
2220
import org.junit.Test;
2321
import org.junit.runner.RunWith;
2422
import org.mockito.Mock;
25-
import org.mockito.invocation.InvocationOnMock;
2623
import org.mockito.runners.MockitoJUnitRunner;
27-
import org.mockito.stubbing.Answer;
2824
import org.slf4j.MDC;
2925

3026
import com.sap.hcp.cf.logging.common.request.HttpHeader;
@@ -42,30 +38,35 @@ public class CorrelationIdFilterTest {
4238
@Mock
4339
private FilterChain chain;
4440

45-
private CorrelationIdFromMDCExtractor mdcExtractor;
41+
private ContextMapExtractor mdcExtractor;
4642

4743
@Before
4844
public void setUp() throws Exception {
49-
mdcExtractor = new CorrelationIdFromMDCExtractor(HttpHeaders.CORRELATION_ID.getField());
45+
MDC.clear();
46+
mdcExtractor = new ContextMapExtractor();
5047
doAnswer(mdcExtractor).when(chain).doFilter(request, response);
5148
}
5249

50+
public String getExtractedCorrelationId() {
51+
return mdcExtractor.getField(HttpHeaders.CORRELATION_ID.getField());
52+
}
53+
5354
@Test
5455
public void addsKnownCorrelationIdToMDC() throws Exception {
5556
when(request.getHeader(HttpHeaders.CORRELATION_ID.getName())).thenReturn(KNOWN_CORRELATION_ID);
5657

5758
new CorrelationIdFilter().doFilter(request, response, chain);
5859

59-
assertThat(mdcExtractor.getCorrelationId(), is(equalTo(KNOWN_CORRELATION_ID)));
60+
assertThat(getExtractedCorrelationId(), is(equalTo(KNOWN_CORRELATION_ID)));
6061
}
6162

6263
@Test
6364
public void addsGeneratedCorrelationIdToMDC() throws Exception {
6465

6566
new CorrelationIdFilter().doFilter(request, response, chain);
6667

67-
assertThat(mdcExtractor.getCorrelationId(), is(not(nullValue())));
68-
assertThat(mdcExtractor.getCorrelationId(), is(not(equalTo(KNOWN_CORRELATION_ID))));
68+
assertThat(getExtractedCorrelationId(), is(not(nullValue())));
69+
assertThat(getExtractedCorrelationId(), is(not(equalTo(KNOWN_CORRELATION_ID))));
6970
}
7071

7172
@Test
@@ -105,65 +106,15 @@ public void doesNotOverwriteCorrelationIdInResponse() throws Exception {
105106
verify(response).getHeader(HttpHeaders.CORRELATION_ID.getName());
106107
verifyNoMoreInteractions(response);
107108
}
108-
109+
109110
@Test
110111
public void usesCustomHeader() throws Exception {
111-
HttpHeader myHeader = new HttpHeader() {
112-
113-
@Override
114-
public boolean isPropagated() {
115-
return true;
116-
}
117-
118-
@Override
119-
public String getName() {
120-
return "my-header";
121-
}
122-
123-
@Override
124-
public String getFieldValue() {
125-
return null;
126-
}
127-
128-
@Override
129-
public String getField() {
130-
return "my-field";
131-
}
132-
133-
@Override
134-
public List<HttpHeader> getAliases() {
135-
return Collections.emptyList();
136-
}
137-
};
112+
HttpHeader myHeader = new HttpTestHeader("my-header", "my-field", null, false);
138113
when(request.getHeader("my-header")).thenReturn(KNOWN_CORRELATION_ID);
139-
CorrelationIdFromMDCExtractor myMdcExtractor = new CorrelationIdFromMDCExtractor("my-field");
140-
doAnswer(myMdcExtractor).when(chain).doFilter(request, response);
141114

142115
new CorrelationIdFilter(myHeader).doFilter(request, response, chain);
143-
144-
assertThat(myMdcExtractor.getCorrelationId(), is(equalTo(KNOWN_CORRELATION_ID)));
145-
verify(response).setHeader("my-header", KNOWN_CORRELATION_ID);
146-
147-
}
148116

149-
private static class CorrelationIdFromMDCExtractor implements Answer<Void> {
150-
151-
private String correlationId;
152-
private String field;
153-
154-
private CorrelationIdFromMDCExtractor(String field) {
155-
this.field = field;
156-
}
157-
158-
public String getCorrelationId() {
159-
return correlationId;
160-
}
161-
162-
@Override
163-
public Void answer(InvocationOnMock invocation) throws Throwable {
164-
correlationId = MDC.get(field);
165-
return null;
166-
}
117+
assertThat(mdcExtractor.getField("my-field"), is(equalTo(KNOWN_CORRELATION_ID)));
118+
verify(response).setHeader("my-header", KNOWN_CORRELATION_ID);
167119
}
168-
169120
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.sap.hcp.cf.logging.servlet.filter;
2+
3+
import java.util.Arrays;
4+
import java.util.List;
5+
6+
import com.sap.hcp.cf.logging.common.request.HttpHeader;
7+
8+
public class HttpTestHeader implements HttpHeader {
9+
10+
private String name;
11+
private String field;
12+
private String fieldValue;
13+
private boolean propagated;
14+
private List<HttpHeader> aliases;
15+
16+
public HttpTestHeader(String name, String field, String fieldValue, boolean propagated, HttpHeader... aliases) {
17+
this.name = name;
18+
this.field = field;
19+
this.fieldValue = fieldValue;
20+
this.propagated = propagated;
21+
this.aliases = Arrays.asList(aliases);
22+
}
23+
24+
@Override
25+
public boolean isPropagated() {
26+
return propagated;
27+
}
28+
29+
@Override
30+
public String getName() {
31+
return name;
32+
}
33+
34+
@Override
35+
public String getField() {
36+
return field;
37+
}
38+
39+
@Override
40+
public List<HttpHeader> getAliases() {
41+
return aliases;
42+
}
43+
44+
@Override
45+
public String getFieldValue() {
46+
return fieldValue;
47+
}
48+
49+
}

0 commit comments

Comments
 (0)