Skip to content

Commit 4ef1dd3

Browse files
Custom json layout for lambda logging (#6)
* Custom json layout for lambda logging
1 parent f552c87 commit 4ef1dd3

File tree

10 files changed

+274
-162
lines changed

10 files changed

+274
-162
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
max-parallel: 4
1414
matrix:
1515
# test against latest update of each major Java version, as well as specific updates of LTS versions:
16-
java: [7, 7.0.181, 8, 8.0.192, 9.0.x, 10, 11.0.x, 11.0.3, 12, 13 ]
16+
java: [8, 8.0.192, 9.0.x, 10, 11.0.x, 11.0.3, 12, 13 ]
1717
name: Java ${{ matrix.java }}
1818
env:
1919
OS: ${{ matrix.os }}

example/HelloWorldFunction/pom.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
<artifactId>aws-lambda-java-events</artifactId>
2929
<version>3.1.0</version>
3030
</dependency>
31-
3231
<dependency>
3332
<groupId>org.apache.logging.log4j</groupId>
3433
<artifactId>log4j-core</artifactId>

example/HelloWorldFunction/src/main/java/helloworld/App.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
1515
import org.apache.logging.log4j.LogManager;
1616
import org.apache.logging.log4j.Logger;
17-
import software.aws.lambda.logging.LambdaJsonAppender;
17+
import org.apache.logging.log4j.ThreadContext;
18+
import software.aws.lambda.logging.DefaultLambdaFields;
1819

1920
/**
2021
* Handler for requests to Lambda function.
@@ -24,7 +25,7 @@ public class App implements RequestHandler<APIGatewayProxyRequestEvent, APIGatew
2425
Logger log = LogManager.getLogger();
2526

2627
public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
27-
LambdaJsonAppender.loadContextKeys(context);
28+
ThreadContext.putAll(DefaultLambdaFields.values(context));
2829

2930
Map<String, String> headers = new HashMap<>();
3031
headers.put("Content-Type", "application/json");
@@ -37,6 +38,7 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv
3738
log.info(pageContents);
3839
String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents);
3940

41+
log.info("After output");
4042
return response
4143
.withStatusCode(200)
4244
.withBody(output);
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Configuration packages="com.amazonaws.services.lambda.runtime.log4j2">
33
<Appenders>
4-
<LambdaJsonAppender name="LambdaJsonAppender" />
4+
<Console name="JsonAppender" target="SYSTEM_OUT">
5+
<LambdaJsonLayout compact="true" eventEol="true"/>
6+
</Console>
57
</Appenders>
68
<Loggers>
9+
<Logger name="JsonLogger" level="INFO" additivity="false">
10+
<AppenderRef ref="JsonAppender"/>
11+
</Logger>
712
<Root level="info">
8-
<AppenderRef ref="LambdaJsonAppender" />
13+
<AppenderRef ref="JsonAppender"/>
914
</Root>
1015
</Loggers>
1116
</Configuration>

pom.xml

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
<properties>
4040
<maven.compiler.source>1.8</maven.compiler.source>
4141
<maven.compiler.target>1.8</maven.compiler.target>
42-
<log4j.version>2.13.2</log4j.version>
42+
<log4j.version>2.13.3</log4j.version>
4343
<jackson.version>2.11.0</jackson.version>
4444
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
4545
</properties>
@@ -67,13 +67,6 @@
6767
<version>${jackson.version}</version>
6868
</dependency>
6969

70-
<!-- TODO - MS is this the correct scope? -->
71-
<dependency>
72-
<groupId>org.projectlombok</groupId>
73-
<artifactId>lombok</artifactId>
74-
<version>1.18.12</version>
75-
<scope>provided</scope>
76-
</dependency>
7770
<dependency>
7871
<groupId>org.apache.logging.log4j</groupId>
7972
<artifactId>log4j-core</artifactId>
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
package org.apache.logging.log4j.core.layout;
2+
3+
import com.fasterxml.jackson.annotation.JsonAnyGetter;
4+
import com.fasterxml.jackson.annotation.JsonGetter;
5+
import com.fasterxml.jackson.annotation.JsonRootName;
6+
import com.fasterxml.jackson.annotation.JsonUnwrapped;
7+
import com.fasterxml.jackson.databind.ObjectMapper;
8+
import org.apache.logging.log4j.core.Layout;
9+
import org.apache.logging.log4j.core.LogEvent;
10+
import org.apache.logging.log4j.core.config.Configuration;
11+
import org.apache.logging.log4j.core.config.DefaultConfiguration;
12+
import org.apache.logging.log4j.core.config.Node;
13+
import org.apache.logging.log4j.core.config.plugins.Plugin;
14+
import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
15+
import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
16+
import org.apache.logging.log4j.core.jackson.Log4jJsonObjectMapper;
17+
import org.apache.logging.log4j.core.jackson.XmlConstants;
18+
import org.apache.logging.log4j.core.util.KeyValuePair;
19+
import org.apache.logging.log4j.util.Strings;
20+
21+
import java.io.IOException;
22+
import java.io.Writer;
23+
import java.nio.charset.Charset;
24+
import java.nio.charset.StandardCharsets;
25+
import java.time.ZoneId;
26+
import java.time.ZonedDateTime;
27+
import java.util.HashMap;
28+
import java.util.LinkedHashMap;
29+
import java.util.Map;
30+
31+
import static java.time.Instant.ofEpochMilli;
32+
import static java.time.format.DateTimeFormatter.ISO_ZONED_DATE_TIME;
33+
34+
@Plugin(name = "LambdaJsonLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
35+
public class LambdaJsonLayout extends AbstractJacksonLayout {
36+
37+
private static final String DEFAULT_FOOTER = "]";
38+
39+
private static final String DEFAULT_HEADER = "[";
40+
41+
static final String CONTENT_TYPE = "application/json";
42+
43+
private ObjectMapper objectMapper;
44+
45+
public static class Builder<B extends Builder<B>> extends AbstractJacksonLayout.Builder<B>
46+
implements org.apache.logging.log4j.core.util.Builder<LambdaJsonLayout> {
47+
48+
@PluginBuilderAttribute
49+
private boolean propertiesAsList;
50+
51+
@PluginBuilderAttribute
52+
private boolean objectMessageAsJsonObject;
53+
54+
public Builder() {
55+
super();
56+
setCharset(StandardCharsets.UTF_8);
57+
}
58+
59+
@Override
60+
public LambdaJsonLayout build() {
61+
final boolean encodeThreadContextAsList = isProperties() && propertiesAsList;
62+
final String headerPattern = toStringOrNull(getHeader());
63+
final String footerPattern = toStringOrNull(getFooter());
64+
return new LambdaJsonLayout(getConfiguration(), isLocationInfo(), isProperties(), encodeThreadContextAsList,
65+
isComplete(), isCompact(), getEventEol(), headerPattern, footerPattern, getCharset(),
66+
isIncludeStacktrace(), isStacktraceAsString(), isIncludeNullDelimiter(),
67+
getAdditionalFields(), getObjectMessageAsJsonObject());
68+
}
69+
70+
public boolean isPropertiesAsList() {
71+
return propertiesAsList;
72+
}
73+
74+
public B setPropertiesAsList(final boolean propertiesAsList) {
75+
this.propertiesAsList = propertiesAsList;
76+
return asBuilder();
77+
}
78+
79+
public boolean getObjectMessageAsJsonObject() {
80+
return objectMessageAsJsonObject;
81+
}
82+
83+
public B setObjectMessageAsJsonObject(final boolean objectMessageAsJsonObject) {
84+
this.objectMessageAsJsonObject = objectMessageAsJsonObject;
85+
return asBuilder();
86+
}
87+
}
88+
89+
private LambdaJsonLayout(final Configuration config, final boolean locationInfo, final boolean properties,
90+
final boolean encodeThreadContextAsList,
91+
final boolean complete, final boolean compact, final boolean eventEol,
92+
final String headerPattern, final String footerPattern, final Charset charset,
93+
final boolean includeStacktrace, final boolean stacktraceAsString,
94+
final boolean includeNullDelimiter,
95+
final KeyValuePair[] additionalFields, final boolean objectMessageAsJsonObject) {
96+
super(config, new JacksonFactory.JSON(encodeThreadContextAsList, includeStacktrace, stacktraceAsString, objectMessageAsJsonObject).newWriter(
97+
locationInfo, properties, compact),
98+
charset, compact, complete, eventEol,
99+
null,
100+
PatternLayout.newSerializerBuilder().setConfiguration(config).setPattern(headerPattern).setDefaultPattern(DEFAULT_HEADER).build(),
101+
PatternLayout.newSerializerBuilder().setConfiguration(config).setPattern(footerPattern).setDefaultPattern(DEFAULT_FOOTER).build(),
102+
includeNullDelimiter,
103+
additionalFields);
104+
this.objectMapper = new Log4jJsonObjectMapper(encodeThreadContextAsList, includeStacktrace, stacktraceAsString, objectMessageAsJsonObject);
105+
}
106+
107+
/**
108+
* Returns appropriate JSON header.
109+
*
110+
* @return a byte array containing the header, opening the JSON array.
111+
*/
112+
@Override
113+
public byte[] getHeader() {
114+
if (!this.complete) {
115+
return null;
116+
}
117+
final StringBuilder buf = new StringBuilder();
118+
final String str = serializeToString(getHeaderSerializer());
119+
if (str != null) {
120+
buf.append(str);
121+
}
122+
buf.append(this.eol);
123+
return getBytes(buf.toString());
124+
}
125+
126+
/**
127+
* Returns appropriate JSON footer.
128+
*
129+
* @return a byte array containing the footer, closing the JSON array.
130+
*/
131+
@Override
132+
public byte[] getFooter() {
133+
if (!this.complete) {
134+
return null;
135+
}
136+
final StringBuilder buf = new StringBuilder();
137+
buf.append(this.eol);
138+
final String str = serializeToString(getFooterSerializer());
139+
if (str != null) {
140+
buf.append(str);
141+
}
142+
buf.append(this.eol);
143+
return getBytes(buf.toString());
144+
}
145+
146+
@Override
147+
public Map<String, String> getContentFormat() {
148+
final Map<String, String> result = new HashMap<>();
149+
result.put("version", "2.0");
150+
return result;
151+
}
152+
153+
/**
154+
* @return The content type.
155+
*/
156+
@Override
157+
public String getContentType() {
158+
return CONTENT_TYPE + "; charset=" + this.getCharset();
159+
}
160+
161+
@PluginBuilderFactory
162+
public static <B extends Builder<B>> B newBuilder() {
163+
return new Builder<B>().asBuilder();
164+
}
165+
166+
/**
167+
* Creates a JSON Layout using the default settings. Useful for testing.
168+
*
169+
* @return A JSON Layout.
170+
*/
171+
public static LambdaJsonLayout createDefaultLayout() {
172+
return new LambdaJsonLayout(new DefaultConfiguration(), false, false, false, false, false, false,
173+
DEFAULT_HEADER, DEFAULT_FOOTER, StandardCharsets.UTF_8, true, false, false, null, false);
174+
}
175+
176+
@Override
177+
public Object wrapLogEvent(final LogEvent event) {
178+
Map<String, Object> additionalFieldsMap = resolveAdditionalFields(event);
179+
// This class combines LogEvent with AdditionalFields during serialization
180+
return new LogEventWithAdditionalFields(event, additionalFieldsMap);
181+
}
182+
183+
@Override
184+
public void toSerializable(final LogEvent event, final Writer writer) throws IOException {
185+
if (complete && eventCount > 0) {
186+
writer.append(", ");
187+
}
188+
super.toSerializable(event, writer);
189+
}
190+
191+
private Map<String, Object> resolveAdditionalFields(LogEvent logEvent) {
192+
// Note: LinkedHashMap retains order
193+
final Map<String, Object> additionalFieldsMap = new LinkedHashMap<>(additionalFields.length);
194+
195+
// Go over MDC
196+
logEvent.getContextData().forEach((key, value) -> {
197+
if (Strings.isNotBlank(key) && value != null) {
198+
additionalFieldsMap.put(key, value);
199+
}
200+
});
201+
202+
return additionalFieldsMap;
203+
}
204+
205+
@JsonRootName(XmlConstants.ELT_EVENT)
206+
public static class LogEventWithAdditionalFields {
207+
208+
private final LogEvent logEvent;
209+
private final Map<String, Object> additionalFields;
210+
211+
public LogEventWithAdditionalFields(LogEvent logEvent, Map<String, Object> additionalFields) {
212+
this.logEvent = logEvent;
213+
this.additionalFields = additionalFields;
214+
}
215+
216+
@JsonUnwrapped
217+
public Object getLogEvent() {
218+
return logEvent;
219+
}
220+
221+
@JsonAnyGetter
222+
public Map<String, Object> getAdditionalFields() {
223+
return additionalFields;
224+
}
225+
226+
@JsonGetter("timestamp")
227+
public String getTimestamp() {
228+
return ISO_ZONED_DATE_TIME.format(ZonedDateTime.from(ofEpochMilli(logEvent.getTimeMillis()).atZone(ZoneId.systemDefault())));
229+
}
230+
}
231+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package software.aws.lambda.logging;
2+
3+
import com.amazonaws.services.lambda.runtime.Context;
4+
5+
import java.util.HashMap;
6+
import java.util.Map;
7+
8+
public enum DefaultLambdaFields {
9+
FUNCTION_NAME("functionName"),
10+
FUNCTION_VERSION("functionVersion"),
11+
FUNCTION_ARN("functionArn"),
12+
FUNCTION_MEMORY_SIZE("functionMemorySize");
13+
14+
private String name;
15+
16+
DefaultLambdaFields(String name) {
17+
this.name = name;
18+
}
19+
20+
public static Map<String, String> values(Context context) {
21+
Map<String, String> hashMap = new HashMap<>();
22+
23+
hashMap.put(FUNCTION_NAME.name, context.getFunctionName());
24+
hashMap.put(FUNCTION_VERSION.name, context.getFunctionVersion());
25+
hashMap.put(FUNCTION_ARN.name, context.getInvokedFunctionArn());
26+
hashMap.put(FUNCTION_MEMORY_SIZE.name, String.valueOf(context.getMemoryLimitInMB()));
27+
28+
return hashMap;
29+
}
30+
}

0 commit comments

Comments
 (0)