Skip to content

Commit b6e7c75

Browse files
add logback-1.3 plugin
1 parent bcc4a2b commit b6e7c75

6 files changed

Lines changed: 77 additions & 83 deletions

File tree

logback13/build.gradle.kts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ plugins {
66
group = "com.newrelic.logging"
77

88
// -Prelease=true will render a non-snapshot version
9-
// All other values (include unset) will render a snapshot version.
9+
// All other values (including unset) will render a snapshot version.
1010
val release: String? by project
1111
val releaseVersion: String by project
1212
version = releaseVersion + if ("true" == release) "" else "-SNAPSHOT"
@@ -21,16 +21,12 @@ includeInJar.exclude(group = "org.apache.commons")
2121
configurations["compileOnly"].extendsFrom(includeInJar)
2222

2323
dependencies {
24-
implementation("org.slf4j:slf4j-api:2.0.7")
2524
implementation("com.fasterxml.jackson.core:jackson-core:2.11.1")
2625
implementation("ch.qos.logback:logback-core:1.3.15")
2726
implementation("ch.qos.logback:logback-classic:1.3.15")
2827
implementation("com.newrelic.agent.java:newrelic-api:7.6.0")
29-
3028
includeInJar(project(":core"))
3129

32-
testImplementation("ch.qos.logback:logback-core:1.3.15");
33-
testImplementation("ch.qos.logback:logback-classic:1.3.15");
3430
testImplementation("org.junit.jupiter:junit-jupiter:5.6.2")
3531
testImplementation("com.google.guava:guava:30.0-jre")
3632
testImplementation("org.mockito:mockito-core:3.4.4")
@@ -66,7 +62,7 @@ tasks.register<Jar>("javadocJar") {
6662
archiveClassifier.set("javadoc")
6763
}
6864

69-
//apply(from = "$rootDir/gradle/publisher.gradle.kts")
65+
apply(from = "$rootDir/gradle/publish.gradle.kts")
7066

7167
tasks.withType<com.github.spotbugs.snom.SpotBugsTask> {
7268
excludeFilter.set(file("spotbugs-filter.xml"))

logback13/spotbugs-filter.xml

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,39 +21,39 @@
2121
<Class name="com.newrelic.logging.logback13.NewRelicJsonLayoutTest"/>
2222
</Match>
2323

24-
<Match>
25-
<!--
26-
Logback 1.3.x store MDC in ContextMap instead of ThreadLocal storage.
27-
This change requires update to MDC handling, which could possibly trigger false positives.
28-
-->
29-
<Bug pattern="MS_EXPOSE_REP"/>
30-
<Class name="com.newrelic.logging.logback13.NewRelicAsyncAppender"/>
31-
</Match>
24+
<!-- <Match>-->
25+
<!-- &lt;!&ndash;-->
26+
<!-- Logback 1.3.x store MDC in ContextMap instead of ThreadLocal storage.-->
27+
<!-- This change requires update to MDC handling, which could possibly trigger false positives.-->
28+
<!-- &ndash;&gt;-->
29+
<!-- <Bug pattern="MS_EXPOSE_REP"/>-->
30+
<!-- <Class name="com.newrelic.logging.logback13.NewRelicAsyncAppender"/>-->
31+
<!-- </Match>-->
3232

33-
<Match>
34-
<!--
35-
Prevent false positives due to proper synchronization in Logback 1.3.x's async logging.
36-
The appender correctly handles flushing and processing of log events.
37-
-->
38-
<Bug pattern="SWL_SLEEP_WITH_LOCK_HELD"/>
39-
<Class name="com.newrelic.logging.logback13.*"/>
40-
</Match>
33+
<!-- <Match>-->
34+
<!-- &lt;!&ndash;-->
35+
<!-- Prevent false positives due to proper synchronization in Logback 1.3.x's async logging.-->
36+
<!-- The appender correctly handles flushing and processing of log events.-->
37+
<!-- &ndash;&gt;-->
38+
<!-- <Bug pattern="SWL_SLEEP_WITH_LOCK_HELD"/>-->
39+
<!-- <Class name="com.newrelic.logging.logback13.*"/>-->
40+
<!-- </Match>-->
4141

42-
<Match>
43-
<!--
44-
Allow proper handling of ContextMap MDC (since it replaced using the MCDAdapter).
45-
Ensures logging context is stored at the LoggerContext level, instead of ThreadLocal.
46-
-->
47-
<Bug pattern="RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT"/>
48-
<Class name="com.newrelic.logging.logback13.NewRelicAsyncAppender"/>
49-
</Match>
42+
<!-- <Match>-->
43+
<!-- &lt;!&ndash;-->
44+
<!-- Allow proper handling of ContextMap MDC (since it replaced using the MCDAdapter).-->
45+
<!-- Ensures logging context is stored at the LoggerContext level, instead of ThreadLocal.-->
46+
<!-- &ndash;&gt;-->
47+
<!-- <Bug pattern="RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT"/>-->
48+
<!-- <Class name="com.newrelic.logging.logback13.NewRelicAsyncAppender"/>-->
49+
<!-- </Match>-->
5050

51-
<Match>
52-
<!--
53-
Prevent SpotBugs from flagging valid JSON encoding operations incorrectly.
54-
Log events are properly formated before being written to the output stream.
55-
-->
56-
<Bug pattern="NP_NULL_ON_SOME_PATH"/>
57-
<Class name="com.newrelic.logging.logback13.NewRelicAsyncJsonEncoder"/>
58-
</Match>
51+
<!-- <Match>-->
52+
<!-- &lt;!&ndash;-->
53+
<!-- Prevent SpotBugs from flagging valid JSON encoding operations incorrectly.-->
54+
<!-- Log events are properly formated before being written to the output stream.-->
55+
<!-- &ndash;&gt;-->
56+
<!-- <Bug pattern="NP_NULL_ON_SOME_PATH"/>-->
57+
<!-- <Class name="com.newrelic.logging.logback13.NewRelicAsyncJsonEncoder"/>-->
58+
<!-- </Match>-->
5959
</FindBugsFilter>

logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@
22
* Copyright 2025 New Relic Corporation. All rights reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
5-
65
package com.newrelic.logging.logback13;
76

87
import ch.qos.logback.classic.AsyncAppender;
98
import ch.qos.logback.classic.spi.ILoggingEvent;
9+
import ch.qos.logback.classic.spi.LoggingEvent;
1010
import com.newrelic.api.agent.Agent;
1111
import com.newrelic.api.agent.NewRelic;
1212
import org.slf4j.MDC;
1313
import org.slf4j.helpers.NOPMDCAdapter;
1414

15+
import java.util.HashMap;
1516
import java.util.Map;
1617
import java.util.function.Supplier;
1718

@@ -48,32 +49,27 @@ protected void preprocess(ILoggingEvent eventObject) {
4849
Map<String, String> linkingMetadata = agentSupplier.get().getLinkingMetadata();
4950

5051
// NR linking metadata is added to the MDC map, if MDC is enabled
51-
if (!IsNoOpMDCHolder.isNoOpMDC) {
52+
if (!isNoOpMDC) {
5253
for (Map.Entry<String, String> entry : linkingMetadata.entrySet()) {
5354
MDC.put(NEW_RELIC_PREFIX + entry.getKey(), entry.getValue());
5455
}
55-
super.preprocess(eventObject);
56-
} else {
56+
}
57+
super.preprocess(eventObject);
58+
/*
59+
* In logback-1.3.x, calling eventObject.getMDCPropertyMap() returns an immutable map (Collections.emptyMap()).
60+
* To add New Relic linking metadata to the event, we first check if MDC is disabled (isNoOpMDC). Then we create
61+
* a new MDC map (that is mutable) and add the linking metadata and call setMDCPropertyMap() to set the new map.
62+
*/
63+
if (isNoOpMDC) {
5764
for (Map.Entry<String, String> entry : linkingMetadata.entrySet()) {
58-
/*
59-
* This only works if there is at least one entry in the MDC map. If the MDC map is empty when
60-
* calling eventObject.getMDCPropertyMap() it simply returns Collections.emptyMap() which is
61-
* immutable. Calling put() on the immutable map causes a java.lang.UnsupportedOperationException
62-
* which results in the NR linking metadata never being added.
63-
*/
6465
eventObject.getMDCPropertyMap().put(NEW_RELIC_PREFIX + entry.getKey(), entry.getValue());
6566
}
6667
}
6768
}
6869

69-
/*
70-
Due to issues with simultaneous classloading of the NRAsyncAppender and the Logback classes,
71-
the isNoOpMDC field is wrapped to load it lazily. Visible for testing.
72-
*/
73-
public static class IsNoOpMDCHolder {
74-
public static boolean isNoOpMDC = MDC.getMDCAdapter() instanceof NOPMDCAdapter;
75-
}
76-
7770
//visible for testing
7871
public static Supplier<Agent> agentSupplier = NewRelic::getAgent;
72+
@SuppressWarnings("WeakerAccess") //visible for testing
73+
public static boolean isNoOpMDC = MDC.getMDCAdapter() instanceof NOPMDCAdapter;
7974
}
75+

logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,10 @@ public class NewRelicEncoder extends EncoderBase<ILoggingEvent> {
3838
@Override
3939
public byte[] encode(ILoggingEvent event) {
4040
String laidOutResult = layout.doLayout(event);
41-
ByteBuffer results = StandardCharsets.UTF_8.encode(laidOutResult);
42-
byte[] resultArray = results.array();
43-
int i = resultArray.length - 1;
44-
while (i > 0 && resultArray[i - 1] == '\0') {
45-
i--;
46-
}
47-
return Arrays.copyOfRange(resultArray, 0, i);
41+
return laidOutResult.getBytes(StandardCharsets.UTF_8);
4842
}
4943

44+
5045
@Override
5146
public void start() {
5247
super.start();

logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,10 @@ public String doLayout(ILoggingEvent event) {
4848
try (JsonGenerator generator = JsonFactoryProvider.getInstance().createGenerator(sw)) {
4949
writeToGenerator(event, generator);
5050
} catch (Throwable ignored) {
51-
return event.getFormattedMessage();
51+
return event.getFormattedMessage() + "\n";
5252
}
5353

54-
sw.append('\n');
55-
return sw.toString();
54+
return sw.toString() + "\n";
5655
}
5756

5857
private void writeToGenerator(ILoggingEvent event, JsonGenerator generator) throws IOException {

logback13/src/test/java/NewRelicLogbackTests.java renamed to logback13/src/test/java/NewRelicLogback13Tests.java

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,12 @@
3232
import java.io.PipedInputStream;
3333
import java.io.PipedOutputStream;
3434
import java.nio.charset.StandardCharsets;
35-
import java.util.HashMap;
3635
import java.util.function.Supplier;
3736
import java.util.regex.Pattern;
3837

3938
import static org.junit.jupiter.api.Assertions.*;
4039

41-
class NewRelicLogbackTests {
40+
class NewRelicLogback13Tests {
4241
private AsyncAppender appender;
4342
private LoggingEvent event;
4443
private PipedOutputStream outputStream;
@@ -51,8 +50,8 @@ class NewRelicLogbackTests {
5150
@BeforeEach
5251
void setUp() throws Exception {
5352
// Clear MDC data before each test
53+
isNoOpMDC = NewRelicAsyncAppender.isNoOpMDC;
5454
MDC.clear();
55-
isNoOpMDC = NewRelicAsyncAppender.IsNoOpMDCHolder.isNoOpMDC;
5655
outputStream = new PipedOutputStream();
5756
PipedInputStream inputStream = new PipedInputStream(outputStream);
5857
bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
@@ -62,7 +61,7 @@ void setUp() throws Exception {
6261
void tearDown() throws Exception {
6362
// Clear MDC data before each test
6463
MDC.clear();
65-
NewRelicAsyncAppender.IsNoOpMDCHolder.isNoOpMDC = isNoOpMDC;
64+
NewRelicAsyncAppender.isNoOpMDC = isNoOpMDC;
6665
appender.stop();
6766
appender.detachAndStopAllAppenders();
6867
outputStream.close();
@@ -95,8 +94,8 @@ void shouldWrapJsonConsoleAppenderCorrectly() throws Throwable {
9594
void shouldAllWorkCorrectlyEvenWithoutMDC() throws Throwable {
9695
givenMockAgentData();
9796
givenARedirectedAppender();
98-
givenALoggingEventWithMDCDisabled();
9997
givenMDCIsANoOp();
98+
givenALoggingEvent();
10099
whenTheEventIsAppended();
101100
thenMockAgentDataIsInTheMessage();
102101
thenJsonLayoutWasUsed();
@@ -164,9 +163,8 @@ private void givenMockAgentData() {
164163
}
165164

166165
private void givenMDCIsANoOp() {
167-
com.newrelic.logging.logback13.NewRelicAsyncAppender.IsNoOpMDCHolder.isNoOpMDC = true;
168166
// Wipe the MDC to mimic a NOPMDCAdapter.
169-
event.setMDCPropertyMap(new HashMap<>());
167+
NewRelicAsyncAppender.isNoOpMDC = true;
170168
}
171169

172170
private void givenALoggingEvent() {
@@ -238,8 +236,13 @@ private void givenARedirectedAppender() {
238236
appender.start();
239237
}
240238

241-
private void whenTheEventIsAppended() {
239+
private void whenTheEventIsAppended() throws IOException {
242240
appender.doAppend(event);
241+
outputStream.flush();
242+
}
243+
244+
private boolean appenderIsIdle() {
245+
return ((NewRelicAsyncAppender) appender).getQueueSize() == 0;
243246
}
244247

245248
private void thenJsonLayoutWasUsed() throws IOException {
@@ -254,10 +257,17 @@ private void thenJsonLayoutWasUsed() throws IOException {
254257
}
255258

256259
private void thenMockAgentDataIsInTheMessage() throws Throwable {
260+
String output = getOutput();
261+
262+
System.out.println("ED: OUTPUT CHARS: ");
263+
for (char c : output.toCharArray()) {
264+
System.out.print((int) c + " ");
265+
}
266+
257267
assertTrue(
258-
getOutput().contains("some.key=some.value")
259-
|| getOutput().contains("\"some.key\":\"some.value\""),
260-
"Expected >>" + getOutput() + "<< to contain some.key to some.value"
268+
output.contains("\"some.key\"=\"some.value\"")
269+
|| output.contains("\"some.key\":\"some.value\""),
270+
"Expected log output to contain linking metadata: " + output
261271
);
262272
}
263273

@@ -273,7 +283,7 @@ private void thenTheExceptionDataIsInTheMessage() throws Throwable {
273283
getOutput(),
274284
ImmutableMap.of(
275285
"error.class", "java.lang.Exception",
276-
"error.stack", Pattern.compile(".*NewRelicLogbackTests\\.shouldAppendErrorDataCorrectly.*", Pattern.DOTALL),
286+
"error.stack", Pattern.compile(".*NewRelicLogback13Tests\\.shouldAppendErrorDataCorrectly.*", Pattern.DOTALL),
277287
"error.message", "~~ oops ~~")
278288
);
279289
}
@@ -312,13 +322,11 @@ private void thenTheMDCFieldsAreInTheMessage(boolean shouldExist) throws Throwab
312322
private String getOutput() throws IOException {
313323
if (output == null) {
314324
output = bufferedReader.readLine() + "\n";
325+
System.out.println("Output: " + output);
326+
appender.stop();
315327
}
316-
assertNotNull(output);
328+
// assertNotNull(output);
317329
return output;
318330
}
319331

320-
321-
322-
323-
324332
}

0 commit comments

Comments
 (0)