Skip to content

Commit ef5e3ca

Browse files
author
Serhii Vydiuk
committed
Implemented data loging, without losing it after business logic processed it
1 parent ba3fe2e commit ef5e3ca

File tree

23 files changed

+307
-139
lines changed

23 files changed

+307
-139
lines changed

packages/java/examples/OwlTestApp/src/main/java/com/readme/example/CustomUserDataCollectorConfig.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package com.readme.example;
22

33
import com.readme.dataextraction.UserDataCollector;
4-
import com.readme.domain.UserData;
5-
import com.readme.starter.datacollection.ServletDataPayloadAdapter;
6-
import org.springframework.context.annotation.Bean;
74
import org.springframework.context.annotation.Configuration;
85

96
/**

packages/java/examples/OwlTestApp/src/main/java/com/readme/example/OwlController.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package com.readme.example;
22

33
import org.springframework.beans.factory.annotation.Value;
4-
import org.springframework.web.bind.annotation.GetMapping;
5-
import org.springframework.web.bind.annotation.PathVariable;
6-
import org.springframework.web.bind.annotation.PutMapping;
7-
import org.springframework.web.bind.annotation.RestController;
4+
import org.springframework.web.bind.annotation.*;
85

96
import java.util.Collection;
107
import java.util.HashMap;
@@ -34,9 +31,10 @@ public Collection<String> getAllOwl() {
3431
}
3532

3633
@PutMapping("/owl/{owlName}")
37-
public String createOwl(@PathVariable String owlName) {
34+
public String createOwl(@PathVariable String owlName, @RequestBody String body) {
3835
UUID owlUuid = UUID.randomUUID();
3936
owlStorage.put(owlUuid.toString(), owlName);
40-
return "Owl " + owlName + " is created wit id: " + owlUuid;
37+
return "Owl " + owlName + " is created wit id: " + owlUuid + "\n" +
38+
" Creation request body: \n" + body;
4139
}
4240
}

packages/java/examples/OwlTestApp/src/main/resources/application.yaml

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
1-
readme:
2-
readmeApiKey: ${README_API_KEY}
3-
userdata:
4-
apiKey:
5-
source: header
6-
fieldName: X-User-Name
7-
email:
8-
source: header
9-
fieldName: X-User-Email
10-
label:
11-
source: header
12-
fieldName: X-User-Id
13-
141
#readme:
152
# readmeApiKey: ${README_API_KEY}
163
# userdata:
174
# apiKey:
18-
# source: jsonBody
19-
# fieldName: /owl-creator/name
5+
# source: header
6+
# fieldName: X-User-Name
207
# email:
21-
# source: jsonBody
22-
# fieldName: /owl-creator/contacts/email
8+
# source: header
9+
# fieldName: X-User-Email
2310
# label:
24-
# source: jsonBody
25-
# fieldName: owl-creator/label
26-
11+
# source: header
12+
# fieldName: X-User-Id
13+
#
14+
readme:
15+
readmeApiKey: ${README_API_KEY}
16+
userdata:
17+
apiKey:
18+
source: jsonBody
19+
fieldName: /owl-creator/name
20+
email:
21+
source: jsonBody
22+
fieldName: /owl-creator/contacts/email
23+
label:
24+
source: jsonBody
25+
fieldName: owl-creator/label
26+
#
2727
#readme:
2828
# readmeApiKey: ${README_API_KEY}
2929
# userdata:

packages/java/readme-metrics-spring-boot-starter/src/main/java/com/readme/starter/datacollection/DataCollectionFilter.java

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import jakarta.servlet.http.HttpServletResponse;
99
import lombok.AllArgsConstructor;
1010
import lombok.extern.slf4j.Slf4j;
11+
import org.springframework.util.StreamUtils;
1112
import org.springframework.web.util.ContentCachingRequestWrapper;
1213
import org.springframework.web.util.ContentCachingResponseWrapper;
1314

@@ -33,38 +34,25 @@ public class DataCollectionFilter implements Filter {
3334
// On the other hand, if we collect a request before doFilter, the response data is not available yet.
3435
@Override
3536
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
36-
HttpServletRequest request = (HttpServletRequest) req;
37-
HttpServletResponse response = (HttpServletResponse) resp;
37+
ContentCachingRequestWrapper request = new ContentCachingRequestWrapper((HttpServletRequest) req);
38+
ContentCachingResponseWrapper response = new ContentCachingResponseWrapper((HttpServletResponse) resp);
3839

3940
try {
4041
if (request.getMethod().equalsIgnoreCase(OPTIONS.name())) {
4142
chain.doFilter(req, resp);
42-
} else if (request.getMethod().equalsIgnoreCase(GET.name())) {
43-
ServletDataPayloadAdapter payload =
44-
new ServletDataPayloadAdapter(request, response);
45-
43+
} else {
4644
//TODO: Handle case if SDK user configured getting request user data from body, but GET req doesn't have it
47-
UserData userData = userDataCollector.collect(payload);
4845
//TODO: Validate user data. Collect request data only if user data is valid ?
49-
requestDataCollector.collect(payload, userData);
50-
chain.doFilter(req, resp);
51-
} else {
52-
ContentCachingRequestWrapper cacheableRequest =
53-
new ContentCachingRequestWrapper(request);
54-
ContentCachingResponseWrapper cacheableResponse =
55-
new ContentCachingResponseWrapper(response);
5646

47+
chain.doFilter(request, response);
5748
ServletDataPayloadAdapter payload =
58-
new ServletDataPayloadAdapter(cacheableRequest, cacheableResponse);
49+
new ServletDataPayloadAdapter(request, response);
5950
UserData userData = userDataCollector.collect(payload);
60-
6151
requestDataCollector.collect(payload, userData);
62-
chain.doFilter(cacheableRequest, cacheableResponse);
52+
response.copyBodyToResponse();
6353
}
6454
} catch (Exception e){
6555
log.error("Error occurred while processing request by readme metrics-sdk: {}", e.getMessage());
66-
} finally {
67-
chain.doFilter(req, resp);
6856
}
6957
}
7058

packages/java/readme-metrics-spring-boot-starter/src/main/java/com/readme/starter/datacollection/ServletDataPayloadAdapter.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
import jakarta.servlet.http.HttpServletRequest;
88
import lombok.extern.slf4j.Slf4j;
9+
import org.springframework.web.util.ContentCachingRequestWrapper;
10+
import org.springframework.web.util.ContentCachingResponseWrapper;
911

1012
import java.io.IOException;
1113
import java.util.*;
@@ -15,8 +17,8 @@
1517
@AllArgsConstructor
1618
public class ServletDataPayloadAdapter implements DataPayloadAdapter {
1719

18-
private HttpServletRequest request;
19-
private HttpServletResponse response;
20+
private ContentCachingRequestWrapper request;
21+
private ContentCachingResponseWrapper response;
2022

2123
//TODO Do I need a separate method to get request parameters?
2224

@@ -33,10 +35,9 @@ public String getRequestContentType() {
3335
@Override
3436
public String getRequestBody() {
3537
try {
36-
return request.getReader()
37-
.lines()
38-
.collect(Collectors.joining(System.lineSeparator()));
39-
} catch (IOException e) {
38+
byte[] contentAsByteArray = request.getContentAsByteArray();
39+
return new String(contentAsByteArray);
40+
} catch (Exception e) {
4041
log.error("Error when trying to get request body: {}", e.getMessage());
4142
}
4243
return "";

packages/java/readme-metrics-spring-boot-starter/src/main/java/com/readme/starter/datacollection/userinfo/ServletUserDataExtractor.java

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.readme.starter.datacollection.ServletDataPayloadAdapter;
88
import lombok.AllArgsConstructor;
99
import lombok.extern.slf4j.Slf4j;
10+
import org.springframework.http.HttpMethod;
1011
import org.springframework.stereotype.Component;
1112
import com.auth0.jwt.JWT;
1213

@@ -35,19 +36,23 @@ public String extractFromHeader(ServletDataPayloadAdapter payload, String fieldN
3536

3637
@Override
3738
public String extractFromBody(ServletDataPayloadAdapter payload, String fieldPath) {
38-
if (payload.getRequestContentType().equalsIgnoreCase("application/json")) {
39-
String requestBody = payload.getRequestBody();
40-
try {
41-
JsonNode currentNode = objectMapper.readTree(requestBody);
42-
if (!fieldPath.startsWith("/")) {
43-
fieldPath = "/" + fieldPath;
39+
if (!payload.getRequestMethod().equalsIgnoreCase(HttpMethod.GET.name())) {
40+
if (payload.getRequestContentType().equalsIgnoreCase("application/json")) {
41+
String requestBody = payload.getRequestBody();
42+
try {
43+
JsonNode currentNode = objectMapper.readTree(requestBody);
44+
if (!fieldPath.startsWith("/")) {
45+
fieldPath = "/" + fieldPath;
46+
}
47+
return currentNode.at(fieldPath).asText();
48+
} catch (Exception e) {
49+
log.error("Error when reading the user data from JSON body: {}", e.getMessage());
4450
}
45-
return currentNode.at(fieldPath).asText();
46-
} catch (Exception e) {
47-
log.error("Error when reading the user data from JSON body: {}", e.getMessage());
4851
}
52+
log.error("The provided body content type {} is not supported to get user data.", payload.getRequestContentType());
53+
return "";
4954
}
50-
log.error("The provided body content type {} is not supported to get user data.", payload.getRequestContentType());
55+
log.error("The HTTP method {} is not supported to get user data from body.", payload.getRequestMethod());
5156
return "";
5257
}
5358

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package com.readme.starter.datacollection;
2+
3+
import com.readme.dataextraction.RequestDataCollector;
4+
import com.readme.dataextraction.UserDataCollector;
5+
import com.readme.domain.UserData;
6+
import jakarta.servlet.FilterChain;
7+
import jakarta.servlet.ServletException;
8+
import jakarta.servlet.http.HttpServletRequest;
9+
import jakarta.servlet.http.HttpServletResponse;
10+
import org.junit.jupiter.api.BeforeEach;
11+
import org.junit.jupiter.api.Test;
12+
import org.mockito.*;
13+
import org.springframework.web.util.ContentCachingRequestWrapper;
14+
import org.springframework.web.util.ContentCachingResponseWrapper;
15+
16+
import java.io.IOException;
17+
18+
import static org.mockito.Mockito.*;
19+
20+
class DataCollectionFilterTest {
21+
22+
@Mock
23+
private RequestDataCollector<ServletDataPayloadAdapter> requestDataCollector;
24+
25+
@Mock
26+
private UserDataCollector<ServletDataPayloadAdapter> userDataCollector;
27+
28+
@Mock
29+
private HttpServletRequest request;
30+
31+
@Mock
32+
private HttpServletResponse response;
33+
34+
@Mock
35+
private FilterChain chain;
36+
37+
private DataCollectionFilter filter;
38+
39+
@BeforeEach
40+
void setUp() {
41+
MockitoAnnotations.openMocks(this);
42+
filter = new DataCollectionFilter(requestDataCollector, userDataCollector);
43+
}
44+
45+
@Test
46+
void doFilter_OptionsRequest_ShouldPassThroughWithoutProcessing() throws Exception {
47+
when(request.getMethod()).thenReturn("OPTIONS");
48+
49+
filter.doFilter(request, response, chain);
50+
51+
verify(chain).doFilter(request, response);
52+
verifyNoInteractions(requestDataCollector, userDataCollector);
53+
}
54+
55+
56+
@Test
57+
void doFilter_GetRequest_ShouldProcessAndCollectData() throws Exception {
58+
when(request.getMethod()).thenReturn("GET");
59+
testChain();
60+
}
61+
62+
@Test
63+
void doFilter_PutRequest_ShouldProcessAndCollectData() throws Exception {
64+
when(request.getMethod()).thenReturn("PUT");
65+
testChain();
66+
}
67+
68+
@Test
69+
void doFilter_PostRequest_ShouldProcessAndCollectData() throws Exception {
70+
when(request.getMethod()).thenReturn("POST");
71+
testChain();
72+
}
73+
74+
@Test
75+
void doFilter_PatchRequest_ShouldProcessAndCollectData() throws Exception {
76+
when(request.getMethod()).thenReturn("PATCH");
77+
testChain();
78+
}
79+
80+
@Test
81+
void doFilter_DeleteRequest_ShouldProcessAndCollectData() throws Exception {
82+
when(request.getMethod()).thenReturn("DELETE");
83+
testChain();
84+
}
85+
86+
87+
private void testChain() throws IOException, ServletException {
88+
UserData userData = getMockedUserData();
89+
when(userDataCollector.collect(any(ServletDataPayloadAdapter.class))).thenReturn(userData);
90+
91+
filter.doFilter(request, response, chain);
92+
93+
verify(chain).doFilter(any(ContentCachingRequestWrapper.class), any(ContentCachingResponseWrapper.class));
94+
95+
ArgumentCaptor<ServletDataPayloadAdapter> payloadCaptor = ArgumentCaptor.forClass(ServletDataPayloadAdapter.class);
96+
97+
verify(userDataCollector).collect(payloadCaptor.capture());
98+
verify(requestDataCollector).collect(eq(payloadCaptor.getValue()), eq(userData));
99+
100+
// TODO Verify response body is copied
101+
// verify(response).getOutputStream();
102+
}
103+
104+
private static UserData getMockedUserData() {
105+
return UserData.builder()
106+
.apiKey("Owl")
107+
108+
.label("owl-label")
109+
.build();
110+
}
111+
112+
@Test
113+
void doFilter_UserDataCollectorThrowsException_ShouldHandleExceptionAndContinueFlow() throws Exception {
114+
when(request.getMethod()).thenReturn("POST");
115+
when(userDataCollector.collect(any(ServletDataPayloadAdapter.class)))
116+
.thenThrow(new RuntimeException("Error in UserDataCollector"));
117+
118+
filter.doFilter(request, response, chain);
119+
120+
verify(chain).doFilter(any(ContentCachingRequestWrapper.class), any(ContentCachingResponseWrapper.class));
121+
verify(requestDataCollector, never()).collect(any(), any());
122+
verifyNoMoreInteractions(requestDataCollector);
123+
}
124+
125+
@Test
126+
void doFilter_RequestDataCollectorThrowsException_ShouldHandleExceptionAndContinueFlow() throws Exception {
127+
when(request.getMethod()).thenReturn("POST");
128+
UserData userData = getMockedUserData();
129+
130+
when(userDataCollector.collect(any(ServletDataPayloadAdapter.class)))
131+
.thenReturn(userData);
132+
doThrow(new RuntimeException("Error in RequestDataCollector"))
133+
.when(requestDataCollector).collect(any(), eq(userData));
134+
135+
filter.doFilter(request, response, chain);
136+
137+
verify(chain).doFilter(any(ContentCachingRequestWrapper.class), any(ContentCachingResponseWrapper.class));
138+
verify(userDataCollector).collect(any(ServletDataPayloadAdapter.class));
139+
}
140+
}

packages/java/readme-metrics-spring-boot-starter/src/test/java/com/readme/starter/datacollection/ServletDataPayloadAdapterTest.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import org.junit.jupiter.api.Test;
77
import org.mockito.Mock;
88
import org.mockito.MockitoAnnotations;
9+
import org.springframework.web.util.ContentCachingRequestWrapper;
10+
import org.springframework.web.util.ContentCachingResponseWrapper;
911

1012
import java.io.BufferedReader;
1113
import java.io.IOException;
@@ -21,10 +23,10 @@
2123
class ServletDataPayloadAdapterTest {
2224

2325
@Mock
24-
private HttpServletRequest requestMock;
26+
private ContentCachingRequestWrapper requestMock;
2527

2628
@Mock
27-
private HttpServletResponse responseMock;
29+
private ContentCachingResponseWrapper responseMock;
2830

2931
private ServletDataPayloadAdapter adapter;
3032

@@ -81,8 +83,8 @@ void getRequestContentType_HappyPath_ReturnsContentType() {
8183
@Test
8284
void getRequestBody_HappyPath_ReturnsRequestBody() throws IOException {
8385
String requestBody = "{\"bird\": \"Owl\"}";
84-
BufferedReader bufferedReader = new BufferedReader(new StringReader(requestBody));
85-
when(requestMock.getReader()).thenReturn(bufferedReader);
86+
byte[] bodyAsBytes = requestBody.getBytes();
87+
when(requestMock.getContentAsByteArray()).thenReturn(bodyAsBytes);
8688
String result = adapter.getRequestBody();
8789

8890
assertEquals(requestBody, result);

0 commit comments

Comments
 (0)