Skip to content

Commit c70c4dc

Browse files
committed
Added request logging from #116. Includes a LogFormatter interface and its default implementation
1 parent eecd30c commit c70c4dc

File tree

8 files changed

+211
-2
lines changed

8 files changed

+211
-2
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.amazonaws.serverless.proxy;
2+
3+
4+
import javax.ws.rs.core.SecurityContext;
5+
6+
7+
/**
8+
* Implementations of the log formatter interface are used by {@link com.amazonaws.serverless.proxy.internal.LambdaContainerHandler} class to log each request
9+
* processed in the container. You can set the log formatter using the {@link com.amazonaws.serverless.proxy.internal.LambdaContainerHandler#setLogFormatter(LogFormatter)}
10+
* method. The servlet implementation of the container ({@link com.amazonaws.serverless.proxy.internal.servlet.AwsLambdaServletContainerHandler} includes a
11+
* default log formatter that produces Apache combined logs. {@link com.amazonaws.serverless.proxy.internal.servlet.ApacheCombinedServletLogFormatter}.
12+
* @param <ContainerRequestType> The request type used by the underlying framework
13+
* @param <ContainerResponseType> The response type produced by the underlying framework
14+
*/
15+
public interface LogFormatter<ContainerRequestType, ContainerResponseType> {
16+
/**
17+
* The format method is called by the container handler to produce the log line that should be written to the logs.
18+
* @param req The incoming request
19+
* @param res The completed response
20+
* @param ctx The security context produced based on the request
21+
* @return The log line
22+
*/
23+
String format(ContainerRequestType req, ContainerResponseType res, SecurityContext ctx);
24+
}

aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
package com.amazonaws.serverless.proxy.internal;
1414

1515

16+
import com.amazonaws.serverless.proxy.LogFormatter;
17+
import com.amazonaws.serverless.proxy.internal.servlet.ApacheCombinedServletLogFormatter;
1618
import com.amazonaws.serverless.proxy.model.ContainerConfig;
1719
import com.amazonaws.serverless.proxy.ExceptionHandler;
1820
import com.amazonaws.serverless.proxy.RequestReader;
@@ -56,10 +58,12 @@ public abstract class LambdaContainerHandler<RequestType, ResponseType, Containe
5658
private ExceptionHandler<ResponseType> exceptionHandler;
5759

5860
protected Context lambdaContext;
61+
protected LogFormatter<ContainerRequestType, ContainerResponseType> logFormatter;
5962

6063
private Logger log = LoggerFactory.getLogger(LambdaContainerHandler.class);
6164

6265

66+
6367
//-------------------------------------------------------------
6468
// Variables - Private - Static
6569
//-------------------------------------------------------------
@@ -119,6 +123,15 @@ public void stripBasePath(String basePath) {
119123
config.setServiceBasePath(basePath);
120124
}
121125

126+
/**
127+
* Sets the formatter used to log request data in CloudWatch. By default this is set to use an Apache
128+
* combined log format based on the servlet request and response object {@link ApacheCombinedServletLogFormatter}.
129+
* @param formatter The log formatter object
130+
*/
131+
public void setLogFormatter(LogFormatter<ContainerRequestType, ContainerResponseType> formatter) {
132+
this.logFormatter = formatter;
133+
}
134+
122135

123136
/**
124137
* Proxies requests to the underlying container given the incoming Lambda request. This method returns a populated
@@ -140,6 +153,10 @@ public ResponseType proxy(RequestType request, Context context) {
140153

141154
latch.await();
142155

156+
if (logFormatter != null) {
157+
log.info(SecurityUtils.crlf(logFormatter.format(containerRequest, containerResponse, securityContext)));
158+
}
159+
143160
return responseWriter.writeResponse(containerResponse, context);
144161
} catch (Exception e) {
145162
log.error("Error while handling request", e);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package com.amazonaws.serverless.proxy.internal.servlet;
2+
3+
4+
import com.amazonaws.serverless.proxy.LogFormatter;
5+
import com.amazonaws.serverless.proxy.model.ApiGatewayRequestContext;
6+
7+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
8+
9+
import javax.servlet.http.HttpServletRequest;
10+
import javax.servlet.http.HttpServletResponse;
11+
import javax.ws.rs.core.SecurityContext;
12+
13+
import java.text.SimpleDateFormat;
14+
import java.util.Calendar;
15+
import java.util.Locale;
16+
17+
import static com.amazonaws.serverless.proxy.RequestReader.API_GATEWAY_CONTEXT_PROPERTY;
18+
19+
20+
/**
21+
* Default implementation of the log formatter. Based on an <code>HttpServletRequest</code> and <code>HttpServletResponse</code> implementations produced
22+
* a log line in the Apache combined log format: https://httpd.apache.org/docs/2.4/logs.html
23+
* @param <ContainerRequestType> An implementation of <code>HttpServletRequest</code>
24+
* @param <ContainerResponseType> An implementation of <code>HttpServletResponse</code>
25+
*/
26+
public class ApacheCombinedServletLogFormatter<ContainerRequestType extends HttpServletRequest, ContainerResponseType extends HttpServletResponse>
27+
implements LogFormatter<ContainerRequestType, ContainerResponseType> {
28+
SimpleDateFormat dateFormat;
29+
30+
public ApacheCombinedServletLogFormatter() {
31+
dateFormat = new SimpleDateFormat("[dd/MM/yyyy:hh:mm:ss Z]");
32+
}
33+
34+
@Override
35+
@SuppressFBWarnings({ "SERVLET_HEADER_REFERER", "SERVLET_HEADER_USER_AGENT" })
36+
public String format(ContainerRequestType servletRequest, ContainerResponseType servletResponse, SecurityContext ctx) {
37+
//LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" combined
38+
StringBuilder logLineBuilder = new StringBuilder();
39+
40+
// %h
41+
logLineBuilder.append(servletRequest.getRemoteAddr());
42+
logLineBuilder.append(" ");
43+
44+
// %l
45+
if (servletRequest instanceof AwsProxyHttpServletRequest && servletRequest.getAttribute(API_GATEWAY_CONTEXT_PROPERTY) != null) {
46+
ApiGatewayRequestContext gatewayContext = (ApiGatewayRequestContext)servletRequest.getAttribute(API_GATEWAY_CONTEXT_PROPERTY);
47+
logLineBuilder.append(gatewayContext.getIdentity().getUserArn());
48+
logLineBuilder.append(" ");
49+
} else {
50+
logLineBuilder.append("- ");
51+
}
52+
53+
// %u
54+
if (ctx != null) {
55+
logLineBuilder.append(ctx.getUserPrincipal().getName());
56+
}
57+
logLineBuilder.append(" ");
58+
59+
60+
// %t
61+
logLineBuilder.append(dateFormat.format(Calendar.getInstance().getTime()));
62+
logLineBuilder.append(" ");
63+
64+
// %r
65+
logLineBuilder.append("\"");
66+
logLineBuilder.append(servletRequest.getMethod().toUpperCase(Locale.ENGLISH));
67+
logLineBuilder.append(" ");
68+
logLineBuilder.append(servletRequest.getPathInfo());
69+
logLineBuilder.append(" ");
70+
logLineBuilder.append(servletRequest.getProtocol());
71+
logLineBuilder.append(" \" ");
72+
73+
// %>s
74+
logLineBuilder.append(servletResponse.getStatus());
75+
logLineBuilder.append(" ");
76+
77+
// %b
78+
if (servletResponse instanceof AwsHttpServletResponse) {
79+
AwsHttpServletResponse awsResponse = (AwsHttpServletResponse)servletResponse;
80+
if (awsResponse.getAwsResponseBodyBytes().length > 0) {
81+
logLineBuilder.append(awsResponse.getAwsResponseBodyBytes().length);
82+
} else {
83+
logLineBuilder.append("-");
84+
}
85+
} else {
86+
logLineBuilder.append("-");
87+
}
88+
logLineBuilder.append(" ");
89+
90+
// \"%{Referer}i\"
91+
logLineBuilder.append("\"");
92+
logLineBuilder.append(servletRequest.getHeader("referer"));
93+
logLineBuilder.append("\"");
94+
95+
// \"%{User-agent}i\"
96+
logLineBuilder.append("\"");
97+
logLineBuilder.append(servletRequest.getHeader("user-agent"));
98+
logLineBuilder.append("\"");
99+
100+
logLineBuilder.append(" combined");
101+
102+
103+
return logLineBuilder.toString();
104+
}
105+
}

aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsLambdaServletContainerHandler.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
import java.io.IOException;
3636

37+
3738
/**
3839
* Abstract extension of the code <code>LambdaContainerHandler</code> object that adds protected variables for the
3940
* <code>ServletContext</code> and <code>FilterChainManager</code>. This object should be extended by the framework-specific
@@ -73,6 +74,8 @@ protected AwsLambdaServletContainerHandler(RequestReader<RequestType, ContainerR
7374
SecurityContextWriter<RequestType> securityContextWriter,
7475
ExceptionHandler<ResponseType> exceptionHandler) {
7576
super(requestReader, responseWriter, securityContextWriter, exceptionHandler);
77+
// set the default log formatter for servlet implementations
78+
setLogFormatter(new ApacheCombinedServletLogFormatter<>());
7679
}
7780

7881
//-------------------------------------------------------------

aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequest.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -501,8 +501,7 @@ public Map<String, String[]> getParameterMap() {
501501

502502
@Override
503503
public String getProtocol() {
504-
// TODO: We should have a cloudfront protocol header
505-
return null;
504+
return request.getRequestContext().getProtocol();
506505
}
507506

508507

@@ -621,6 +620,14 @@ public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse se
621620

622621

623622
private String getHeaderCaseInsensitive(String key) {
623+
// special cases for referer and user agent headers
624+
if ("referer".equals(key.toLowerCase(Locale.ENGLISH))) {
625+
return request.getRequestContext().getIdentity().getCaller();
626+
}
627+
if ("user-agent".equals(key.toLowerCase(Locale.ENGLISH))) {
628+
return request.getRequestContext().getIdentity().getUserAgent();
629+
}
630+
624631
if (request.getHeaders() == null) {
625632
return null;
626633
}

aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/testutils/AwsProxyRequestBuilder.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ public AwsProxyRequestBuilder(String path, String httpMethod) {
6868
this.request.setRequestContext(new ApiGatewayRequestContext());
6969
this.request.getRequestContext().setRequestId("test-invoke-request");
7070
this.request.getRequestContext().setStage("test");
71+
this.request.getRequestContext().setProtocol("HTTP/1.1");
7172
ApiGatewayRequestIdentity identity = new ApiGatewayRequestIdentity();
7273
identity.setSourceIp("127.0.0.1");
7374
this.request.getRequestContext().setIdentity(identity);
@@ -236,6 +237,30 @@ public AwsProxyRequestBuilder serverName(String serverName) {
236237
return this;
237238
}
238239

240+
public AwsProxyRequestBuilder userAgent(String agent) {
241+
if (request.getRequestContext() == null) {
242+
request.setRequestContext(new ApiGatewayRequestContext());
243+
}
244+
if (request.getRequestContext().getIdentity() == null) {
245+
request.getRequestContext().setIdentity(new ApiGatewayRequestIdentity());
246+
}
247+
248+
request.getRequestContext().getIdentity().setUserAgent(agent);
249+
return this;
250+
}
251+
252+
public AwsProxyRequestBuilder referer(String referer) {
253+
if (request.getRequestContext() == null) {
254+
request.setRequestContext(new ApiGatewayRequestContext());
255+
}
256+
if (request.getRequestContext().getIdentity() == null) {
257+
request.getRequestContext().setIdentity(new ApiGatewayRequestIdentity());
258+
}
259+
260+
request.getRequestContext().getIdentity().setCaller(referer);
261+
return this;
262+
}
263+
239264
public AwsProxyRequestBuilder fromJsonString(String jsonContent)
240265
throws IOException {
241266
request = LambdaContainerHandler.getObjectMapper().readValue(jsonContent, AwsProxyRequest.class);

aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/ApiGatewayRequestContext.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public class ApiGatewayRequestContext {
4343
private ApiGatewayAuthorizerContext authorizer;
4444
private String stage;
4545
private String path;
46+
private String protocol;
4647

4748

4849
//-------------------------------------------------------------
@@ -146,4 +147,14 @@ public ApiGatewayAuthorizerContext getAuthorizer() {
146147
public void setAuthorizer(ApiGatewayAuthorizerContext authorizer) {
147148
this.authorizer = authorizer;
148149
}
150+
151+
152+
public String getProtocol() {
153+
return protocol;
154+
}
155+
156+
157+
public void setProtocol(String protocol) {
158+
this.protocol = protocol;
159+
}
149160
}

aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequestTest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public class AwsProxyHttpServletRequestTest {
2323
private static final String FORM_PARAM_TEST = "test_cookie_param";
2424
private static final String QUERY_STRING_NAME_VALUE = "Bob";
2525
private static final String REQUEST_SCHEME_HTTP = "http";
26+
private static final String USER_AGENT = "Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0";
27+
private static final String REFERER = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent/Firefox";
2628

2729
private static final AwsProxyRequest REQUEST_WITH_HEADERS = new AwsProxyRequestBuilder("/hello", "GET")
2830
.header(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE)
@@ -46,6 +48,9 @@ public class AwsProxyHttpServletRequestTest {
4648
private static final AwsProxyRequest REQUEST_MULTIPLE_FORM_AND_QUERY = new AwsProxyRequestBuilder("/hello", "POST")
4749
.form(FORM_PARAM_NAME, FORM_PARAM_NAME_VALUE)
4850
.queryString(FORM_PARAM_TEST, QUERY_STRING_NAME_VALUE).build();
51+
private static final AwsProxyRequest REQUEST_USER_AGENT_REFERER = new AwsProxyRequestBuilder("/hello", "POST")
52+
.userAgent(USER_AGENT)
53+
.referer(REFERER).build();
4954

5055
private static final AwsProxyRequest REQUEST_NULL_QUERY_STRING;
5156
static {
@@ -66,6 +71,18 @@ public void headers_getHeader_validRequest() {
6671
assertEquals(MediaType.APPLICATION_JSON, request.getContentType());
6772
}
6873

74+
@Test
75+
public void headers_getRefererAndUserAgent_returnsContextValues() {
76+
HttpServletRequest request = new AwsProxyHttpServletRequest(REQUEST_USER_AGENT_REFERER, null, null);
77+
assertNotNull(request.getHeader("Referer"));
78+
assertEquals(REFERER, request.getHeader("Referer"));
79+
assertEquals(REFERER, request.getHeader("referer"));
80+
81+
assertNotNull(request.getHeader("User-Agent"));
82+
assertEquals(USER_AGENT, request.getHeader("User-Agent"));
83+
assertEquals(USER_AGENT, request.getHeader("user-agent"));
84+
}
85+
6986
@Test
7087
public void formParams_getParameter_validForm() {
7188
HttpServletRequest request = new AwsProxyHttpServletRequest(REQUEST_FORM_URLENCODED, null, null);

0 commit comments

Comments
 (0)