Skip to content

Commit f28396b

Browse files
authored
Log4j2 extensions (#1344)
* Initial commit of ContextDataProvider for Log4j 2.x * add initial OpenTelemetryJsonLayout implementation * move and reformat * add javadoc * update formatting * fixes for errorprone warnings * integrate feedback for TraceContextDataProvider * integrate feedback for OpenTelemetryJsonLayout * test reformat * check span validity rather than isRecording
1 parent 241017b commit f28396b

File tree

11 files changed

+579
-5
lines changed

11 files changed

+579
-5
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
OpenTelemetry Extensions for Log4j 2.x
2+
======================================================
3+
4+
[![Javadocs][javadoc-image]][javadoc-url]
5+
6+
This module contains extensions that support adding trace correlation information to your
7+
Log4j logs. Several modules are included.
8+
9+
# Context Data Provider
10+
11+
An implementation of the Log4J `ContextDataProvider` class is included. This class is loaded
12+
automatically via the Java ServiceProvider interface, so it works as long as this library is in
13+
your runtime classpath. This class will add three fields to the [thread context](https://logging.apache.org/log4j/2.x/manual/thread-context.html)
14+
for each log entry that happens within an open span. These fields can be addressed from Log4j
15+
layouts. For example, this `PatternLayout` configuration will add request correlation fields to
16+
your logs:
17+
18+
```xml
19+
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} traceid='%X{traceid}' spanid='%X{spanid}' traceflags='%X{traceflags}' - %msg%n"/>
20+
```
21+
22+
Similarly request correlation flags can be added to the JsonLayout:
23+
24+
```xml
25+
<JsonLayout complete="false" compact="true">
26+
<KeyValuePair key="traceid" value="$${ctx:traceid}"/>
27+
<KeyValuePair key="spanid" value="$${ctx:spanid}"/>
28+
<KeyValuePair key="traceflags" value="$${ctx:traceflags}"/>
29+
</JsonLayout>
30+
```
31+
32+
# Open Telemetry JSON Layout
33+
34+
This module also includes a layout component, though it's output format is
35+
provisional and subject to change. To enable it, you must add
36+
`io.opentelemetry.contrib.logging.log4j2` to the `packages` attribute of the
37+
`Configuration` element in your log4j configuration file. You can then use
38+
the `<OpenTelemetryJsonLayout/>` element as a layout. An example configuration
39+
to output to standard output would be:
40+
41+
```xml
42+
<Configuration status="WARN" packages="io.opentelemetry.contrib.logging.log4j2">
43+
<Appenders>
44+
<Console name="stdout">
45+
<OpenTelemetryJsonLayout/>
46+
</Console>
47+
</Appenders>
48+
<Loggers>
49+
<Root level="info">
50+
<AppenderRef ref="stdout"/>
51+
</Root>
52+
</Loggers>
53+
</Configuration>
54+
```
55+
56+
[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-contrib-logging-log4j2-extensions.svg
57+
[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-contrib-logging-log4j2-extensions
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
plugins {
2+
id "java"
3+
id "maven-publish"
4+
5+
id "ru.vyarus.animalsniffer"
6+
}
7+
8+
description = 'OpenTelemetry Contrib Log4j 2.x Extensions'
9+
ext.moduleName = "io.opentelemetry.extensions.logging.log4j2"
10+
11+
dependencies {
12+
api project(':opentelemetry-api')
13+
api group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.13.3'
14+
api group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.13.3'
15+
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.10.4'
16+
17+
testImplementation project(':opentelemetry-sdk')
18+
testImplementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.13.3', classifier: 'tests'
19+
testImplementation group: 'com.google.code.gson', name: 'gson', version: '2.8.6'
20+
// For log4j async
21+
testImplementation group: 'com.lmax', name: 'disruptor', version: '3.4.2'
22+
23+
annotationProcessor libraries.auto_value
24+
25+
signature "org.codehaus.mojo.signature:java17:1.0@signature"
26+
signature "net.sf.androidscents.signature:android-api-level-24:7.0_r2@signature"
27+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright 2020, OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.opentelemetry.contrib.logging.log4j2;
18+
19+
import com.fasterxml.jackson.core.JsonFactory;
20+
import com.fasterxml.jackson.core.JsonGenerator;
21+
import java.io.IOException;
22+
import java.nio.charset.StandardCharsets;
23+
import org.apache.logging.log4j.Level;
24+
import org.apache.logging.log4j.core.LogEvent;
25+
import org.apache.logging.log4j.core.config.plugins.Plugin;
26+
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
27+
import org.apache.logging.log4j.core.layout.AbstractStringLayout;
28+
import org.apache.logging.log4j.core.time.Instant;
29+
import org.apache.logging.log4j.core.util.StringBuilderWriter;
30+
import org.apache.logging.log4j.spi.StandardLevel;
31+
import org.apache.logging.log4j.util.ReadOnlyStringMap;
32+
import org.apache.logging.log4j.util.Strings;
33+
34+
/**
35+
* This class implements a JSON layout for Log4j 2.x that will include request correlation
36+
* information in the form of traceid, spanid, and traceflags fields. The format is provisional, but
37+
* is designed to mirror the <a
38+
* href="https://github.com/open-telemetry/oteps/blob/master/text/logs/0097-log-data-model.md">Log
39+
* Data Model</a>
40+
*/
41+
@Plugin(name = "OpenTelemetryJsonLayout", category = "Core", elementType = "layout")
42+
public class OpenTelemetryJsonLayout extends AbstractStringLayout {
43+
JsonFactory factory = new JsonFactory();
44+
45+
protected OpenTelemetryJsonLayout() {
46+
super(StandardCharsets.UTF_8);
47+
}
48+
49+
@Override
50+
public String toSerializable(LogEvent event) {
51+
StringBuilderWriter writer = new StringBuilderWriter();
52+
try {
53+
ReadOnlyStringMap contextData = event.getContextData();
54+
JsonGenerator generator = factory.createGenerator(writer);
55+
generator.writeStartObject();
56+
writeTimestamp(generator, event.getInstant());
57+
58+
generator.writeFieldName("name");
59+
generator.writeString(event.getLoggerName());
60+
61+
generator.writeFieldName("body");
62+
generator.writeString(event.getMessage().getFormattedMessage());
63+
64+
generator.writeFieldName("severitytext");
65+
generator.writeString(event.getLevel().name());
66+
67+
generator.writeFieldName("severitynumber");
68+
generator.writeNumber(decodeSeverity(event.getLevel()));
69+
70+
if (contextData.containsKey("traceid")) {
71+
writeRequestCorrelation(generator, contextData);
72+
}
73+
generator.writeEndObject();
74+
generator.close();
75+
return writer.toString();
76+
} catch (IOException e) {
77+
LOGGER.error(e);
78+
return Strings.EMPTY;
79+
}
80+
}
81+
82+
private static int decodeSeverity(Level level) {
83+
int intLevel = level.intLevel();
84+
if (intLevel <= StandardLevel.FATAL.intLevel() && intLevel > 0) {
85+
return OpenTelemetryLogLevels.FATAL.asInt();
86+
} else if (intLevel <= StandardLevel.ERROR.intLevel()) {
87+
return OpenTelemetryLogLevels.ERROR.asInt();
88+
} else if (intLevel <= StandardLevel.WARN.intLevel()) {
89+
return OpenTelemetryLogLevels.WARN.asInt();
90+
} else if (intLevel <= StandardLevel.INFO.intLevel()) {
91+
return OpenTelemetryLogLevels.INFO.asInt();
92+
} else if (intLevel <= StandardLevel.DEBUG.intLevel()) {
93+
return OpenTelemetryLogLevels.DEBUG.asInt();
94+
} else if (intLevel <= StandardLevel.TRACE.intLevel()) {
95+
return OpenTelemetryLogLevels.TRACE.asInt();
96+
} else {
97+
return OpenTelemetryLogLevels.UNSET.asInt();
98+
}
99+
}
100+
101+
private static void writeRequestCorrelation(
102+
JsonGenerator generator, ReadOnlyStringMap contextData) throws IOException {
103+
generator.writeFieldName("traceid");
104+
generator.writeString(contextData.getValue("traceid").toString());
105+
generator.writeFieldName("spanid");
106+
generator.writeString(contextData.getValue("spanid").toString());
107+
generator.writeFieldName("traceflags");
108+
generator.writeNumber(contextData.getValue("traceflags").toString());
109+
}
110+
111+
private static void writeTimestamp(JsonGenerator generator, Instant instant) throws IOException {
112+
generator.writeFieldName("timestamp");
113+
generator.writeStartObject();
114+
generator.writeFieldName("millis");
115+
generator.writeNumber(instant.getEpochMillisecond());
116+
int nanos = instant.getNanoOfMillisecond();
117+
if (nanos > 0) {
118+
generator.writeFieldName("nanos");
119+
generator.writeNumber(instant.getNanoOfMillisecond());
120+
}
121+
generator.writeEndObject();
122+
}
123+
124+
@PluginFactory
125+
public static OpenTelemetryJsonLayout build() {
126+
return new OpenTelemetryJsonLayout();
127+
}
128+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2020, OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.opentelemetry.contrib.logging.log4j2;
18+
19+
/**
20+
* This enumerates the log levels defined in the <a
21+
* href="https://github.com/open-telemetry/oteps/blob/master/text/logs/0097-log-data-model.md">log
22+
* data model</a>.
23+
*/
24+
public enum OpenTelemetryLogLevels {
25+
UNSET(0),
26+
TRACE(1),
27+
TRACE2(2),
28+
TRACE3(3),
29+
TRACE4(4),
30+
DEBUG(5),
31+
DEBUG2(6),
32+
DEBUG3(7),
33+
DEBUG4(8),
34+
INFO(9),
35+
INFO2(10),
36+
INFO3(11),
37+
INFO4(12),
38+
WARN(13),
39+
WARN2(14),
40+
WARN3(15),
41+
WARN4(16),
42+
ERROR(17),
43+
ERROR2(18),
44+
ERROR3(19),
45+
ERROR4(20),
46+
FATAL(21),
47+
FATAL2(22),
48+
FATAL3(23),
49+
FATAL4(24),
50+
;
51+
52+
private final int intLevel;
53+
54+
OpenTelemetryLogLevels(int intLevel) {
55+
this.intLevel = intLevel;
56+
}
57+
58+
public int asInt() {
59+
return intLevel;
60+
}
61+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2020, OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.opentelemetry.contrib.logging.log4j2;
18+
19+
import io.opentelemetry.trace.Span;
20+
import io.opentelemetry.trace.SpanContext;
21+
import io.opentelemetry.trace.TracingContextUtils;
22+
import java.util.HashMap;
23+
import java.util.Map;
24+
import org.apache.logging.log4j.core.util.ContextDataProvider;
25+
26+
/**
27+
* This ContextDataProvider is loaded via the ServiceProvider facility. {@link #supplyContextData()}
28+
* is called when a log entry is created.
29+
*/
30+
public class TraceContextDataProvider implements ContextDataProvider {
31+
/**
32+
* This method is called on the creation of a log event, and is called in the same thread as the
33+
* call to the logger. This allows us to pull out request correlation information and make it
34+
* available to a layout, even if the logger is using an {@link
35+
* org.apache.logging.log4j.core.appender.AsyncAppender}
36+
*
37+
* @return A map containing string versions of the traceid, spanid, and traceflags which can then
38+
* be accessed from layout components
39+
* @see OpenTelemetryJsonLayout
40+
*/
41+
@Override
42+
public Map<String, String> supplyContextData() {
43+
Span span = TracingContextUtils.getCurrentSpan();
44+
Map<String, String> map = new HashMap<>();
45+
if (span != null && span.getContext().isValid()) {
46+
SpanContext context = span.getContext();
47+
if (context != null && context.isValid()) {
48+
map.put("traceid", span.getContext().getTraceId().toLowerBase16());
49+
map.put("spanid", span.getContext().getSpanId().toLowerBase16());
50+
map.put("traceflags", span.getContext().getTraceFlags().toLowerBase16());
51+
}
52+
}
53+
return map;
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
io.opentelemetry.contrib.logging.log4j2.TraceContextDataProvider

0 commit comments

Comments
 (0)