From 8dfcd59ea6f3af5ceb2d23c0a3b1ee236e686bbc Mon Sep 17 00:00:00 2001 From: edeleon Date: Fri, 21 Mar 2025 14:32:11 -0700 Subject: [PATCH 01/41] add logback13 plugin --- settings.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/settings.gradle b/settings.gradle index 7388ca2..e7fc11d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,6 +3,7 @@ include 'log4j1' include 'log4j2' include 'logback' include 'logback11' +include 'logback13' include 'jul' include 'dropwizard' include 'core' @@ -16,3 +17,4 @@ include 'examples:jul-app' include 'examples:dropwizard-app' include 'performance:log4j2-perf' + From bcc4a2b3f5f495cc29a85cefde4f759e233a7cd5 Mon Sep 17 00:00:00 2001 From: edeleon Date: Fri, 21 Mar 2025 14:33:03 -0700 Subject: [PATCH 02/41] add initial logback13 plugin --- logback13/build.gradle.kts | 76 ++++ logback13/spotbugs-filter.xml | 59 ++++ .../logging/logback13/CustomArgument.java | 23 ++ .../logback13/JsonFactoryProvider.java | 15 + .../logback13/NewRelicAsyncAppender.java | 79 +++++ .../logging/logback13/NewRelicEncoder.java | 70 ++++ .../logging/logback13/NewRelicJsonLayout.java | 220 ++++++++++++ .../test/java/JsonFactoryProviderTest.java | 33 ++ .../src/test/java/NewRelicJsonLayoutTest.java | 39 +++ .../src/test/java/NewRelicLogbackTests.java | 324 ++++++++++++++++++ 10 files changed, 938 insertions(+) create mode 100644 logback13/build.gradle.kts create mode 100644 logback13/spotbugs-filter.xml create mode 100644 logback13/src/main/java/com/newrelic/logging/logback13/CustomArgument.java create mode 100644 logback13/src/main/java/com/newrelic/logging/logback13/JsonFactoryProvider.java create mode 100644 logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java create mode 100644 logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java create mode 100644 logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java create mode 100644 logback13/src/test/java/JsonFactoryProviderTest.java create mode 100644 logback13/src/test/java/NewRelicJsonLayoutTest.java create mode 100644 logback13/src/test/java/NewRelicLogbackTests.java diff --git a/logback13/build.gradle.kts b/logback13/build.gradle.kts new file mode 100644 index 0000000..6fd5eab --- /dev/null +++ b/logback13/build.gradle.kts @@ -0,0 +1,76 @@ +plugins { + java + id("com.github.spotbugs").version("4.4.4") +} + +group = "com.newrelic.logging" + +// -Prelease=true will render a non-snapshot version +// All other values (include unset) will render a snapshot version. +val release: String? by project +val releaseVersion: String by project +version = releaseVersion + if ("true" == release) "" else "-SNAPSHOT" + +repositories { + mavenCentral() + maven(url = "https://dl.bintray.com/mockito/maven/") +} + +val includeInJar: Configuration by configurations.creating +includeInJar.exclude(group = "org.apache.commons") +configurations["compileOnly"].extendsFrom(includeInJar) + +dependencies { + implementation("org.slf4j:slf4j-api:2.0.7") + implementation("com.fasterxml.jackson.core:jackson-core:2.11.1") + implementation("ch.qos.logback:logback-core:1.3.15") + implementation("ch.qos.logback:logback-classic:1.3.15") + implementation("com.newrelic.agent.java:newrelic-api:7.6.0") + + includeInJar(project(":core")) + + testImplementation("ch.qos.logback:logback-core:1.3.15"); + testImplementation("ch.qos.logback:logback-classic:1.3.15"); + testImplementation("org.junit.jupiter:junit-jupiter:5.6.2") + testImplementation("com.google.guava:guava:30.0-jre") + testImplementation("org.mockito:mockito-core:3.4.4") + testImplementation(project(":core-test")) +} + +val jar by tasks.getting(Jar::class) { + from(configurations["includeInJar"].flatMap { + when { + it.isDirectory -> listOf(it) + else -> listOf(zipTree(it)) + } + }) +} + +tasks.withType { + enabled = true + (options as? CoreJavadocOptions)?.addStringOption("link", "https://logback.qos.ch/apidocs") +} + +configure { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.register("sourcesJar") { + from(sourceSets.main.get().allJava) + archiveClassifier.set("sources") +} + +tasks.register("javadocJar") { + from(tasks.javadoc) + archiveClassifier.set("javadoc") +} + +//apply(from = "$rootDir/gradle/publisher.gradle.kts") + +tasks.withType { + excludeFilter.set(file("spotbugs-filter.xml")) + reports.create("html") { + isEnabled = true + } +} \ No newline at end of file diff --git a/logback13/spotbugs-filter.xml b/logback13/spotbugs-filter.xml new file mode 100644 index 0000000..123384c --- /dev/null +++ b/logback13/spotbugs-filter.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/CustomArgument.java b/logback13/src/main/java/com/newrelic/logging/logback13/CustomArgument.java new file mode 100644 index 0000000..e34c556 --- /dev/null +++ b/logback13/src/main/java/com/newrelic/logging/logback13/CustomArgument.java @@ -0,0 +1,23 @@ +package com.newrelic.logging.logback13; + +public class CustomArgument { + private final String key; + private final String value; + + public CustomArgument(String key, String value) { + this.key = key; + this.value = value; + } + + public static CustomArgument keyValue(String key, String value) { + return new CustomArgument(key, value); + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } +} diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/JsonFactoryProvider.java b/logback13/src/main/java/com/newrelic/logging/logback13/JsonFactoryProvider.java new file mode 100644 index 0000000..3dbfba4 --- /dev/null +++ b/logback13/src/main/java/com/newrelic/logging/logback13/JsonFactoryProvider.java @@ -0,0 +1,15 @@ +package com.newrelic.logging.logback13; + +import com.fasterxml.jackson.core.JsonFactory; + +public class JsonFactoryProvider { + private static final JsonFactory jsonFactory = new JsonFactory(); + + public static JsonFactory getInstance() { + return jsonFactory; + } + + private JsonFactoryProvider() { + + } +} diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java new file mode 100644 index 0000000..ac62135 --- /dev/null +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java @@ -0,0 +1,79 @@ +/* + * Copyright 2025 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.newrelic.logging.logback13; + +import ch.qos.logback.classic.AsyncAppender; +import ch.qos.logback.classic.spi.ILoggingEvent; +import com.newrelic.api.agent.Agent; +import com.newrelic.api.agent.NewRelic; +import org.slf4j.MDC; +import org.slf4j.helpers.NOPMDCAdapter; + +import java.util.Map; +import java.util.function.Supplier; + +/** + * An {@link AsyncAppender} implementation that synchronously captures New Relic trace data. + *

+ * This appender will wrap the existing {@link AsyncAppender} logic in order to capture New Relic data + * on the same thread as the log message was created. To use, wrap your existing appender in your + * config xml, and use the async appender in the appropriate logger. + * + *

{@code
+ *     
+ *         
+ *     
+ *
+ *     
+ *         
+ *     
+ * }
+ * + * @see Logback AsyncAppender + */ +public class NewRelicAsyncAppender extends AsyncAppender { + public static final String NEW_RELIC_PREFIX = "NewRelic:"; + + // required for logback-1.3.x compatibility + @Override + public void start() { + super.start(); + } + + @Override + protected void preprocess(ILoggingEvent eventObject) { + Map linkingMetadata = agentSupplier.get().getLinkingMetadata(); + + // NR linking metadata is added to the MDC map, if MDC is enabled + if (!IsNoOpMDCHolder.isNoOpMDC) { + for (Map.Entry entry : linkingMetadata.entrySet()) { + MDC.put(NEW_RELIC_PREFIX + entry.getKey(), entry.getValue()); + } + super.preprocess(eventObject); + } else { + for (Map.Entry entry : linkingMetadata.entrySet()) { + /* + * This only works if there is at least one entry in the MDC map. If the MDC map is empty when + * calling eventObject.getMDCPropertyMap() it simply returns Collections.emptyMap() which is + * immutable. Calling put() on the immutable map causes a java.lang.UnsupportedOperationException + * which results in the NR linking metadata never being added. + */ + eventObject.getMDCPropertyMap().put(NEW_RELIC_PREFIX + entry.getKey(), entry.getValue()); + } + } + } + + /* + Due to issues with simultaneous classloading of the NRAsyncAppender and the Logback classes, + the isNoOpMDC field is wrapped to load it lazily. Visible for testing. + */ + public static class IsNoOpMDCHolder { + public static boolean isNoOpMDC = MDC.getMDCAdapter() instanceof NOPMDCAdapter; + } + + //visible for testing + public static Supplier agentSupplier = NewRelic::getAgent; +} diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java new file mode 100644 index 0000000..e608abe --- /dev/null +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java @@ -0,0 +1,70 @@ +/* + * Copyright 2025. New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.newrelic.logging.logback13; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.encoder.Encoder; +import ch.qos.logback.core.encoder.EncoderBase; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import static com.newrelic.logging.core.LogExtensionConfig.getMaxStackSize; + +/** + * An {@link ch.qos.logback.core.encoder.Encoder} that will write New Relic's JSON format. + * + * To use, set this as an encoder on an appender using {@link ch.qos.logback.core.OutputStreamAppender#setEncoder(Encoder)}. + * (This is the base class for both {@link ch.qos.logback.core.rolling.RollingFileAppender} and {@link ch.qos.logback.core.ConsoleAppender}.) + * + *
{@code
+ *     
+ *         logs/app-log-file.log
+ *         
+ *     
+ * }
+ * + * @see Logback Encoders + */ +public class NewRelicEncoder extends EncoderBase { + private NewRelicJsonLayout layout; + + private Integer maxStackSize = getMaxStackSize(); + + @Override + public byte[] encode(ILoggingEvent event) { + String laidOutResult = layout.doLayout(event); + ByteBuffer results = StandardCharsets.UTF_8.encode(laidOutResult); + byte[] resultArray = results.array(); + int i = resultArray.length - 1; + while (i > 0 && resultArray[i - 1] == '\0') { + i--; + } + return Arrays.copyOfRange(resultArray, 0, i); + } + + @Override + public void start() { + super.start(); + layout = new NewRelicJsonLayout(maxStackSize); + layout.start(); + } + + public void setMaxStackSize(final Integer maxStackSize) { + this.maxStackSize = maxStackSize; + } + + @Override + public byte[] headerBytes() { + return new byte[0]; + } + + @Override + public byte[] footerBytes() { + return new byte[0]; + } +} \ No newline at end of file diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java new file mode 100644 index 0000000..1dc609d --- /dev/null +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java @@ -0,0 +1,220 @@ +/* + * Copyright 2025. New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.newrelic.logging.logback13; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.IThrowableProxy; +import ch.qos.logback.classic.spi.ThrowableProxyUtil; +import ch.qos.logback.core.Context; +import ch.qos.logback.core.Layout; +import ch.qos.logback.core.status.ErrorStatus; +import ch.qos.logback.core.status.InfoStatus; +import ch.qos.logback.core.status.Status; +import ch.qos.logback.core.status.WarnStatus; +import com.fasterxml.jackson.core.JsonGenerator; +import com.newrelic.logging.core.ElementName; +import com.newrelic.logging.core.ExceptionUtil; +import com.newrelic.logging.core.LogExtensionConfig; +import org.slf4j.Marker; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.Map; + +import static com.newrelic.logging.core.LogExtensionConfig.CONTEXT_PREFIX; +import static com.newrelic.logging.core.LogExtensionConfig.getMaxStackSize; +import static com.newrelic.logging.logback13.NewRelicAsyncAppender.NEW_RELIC_PREFIX; + +public class NewRelicJsonLayout implements Layout { + private final Integer maxStackSize; + private boolean started = false; + private Context context; + + public NewRelicJsonLayout() { + this(getMaxStackSize()); + } + + public NewRelicJsonLayout(Integer maxStackSize) { + this.maxStackSize = maxStackSize; + } + + @Override + public String doLayout(ILoggingEvent event) { + StringWriter sw = new StringWriter(); + + try (JsonGenerator generator = JsonFactoryProvider.getInstance().createGenerator(sw)) { + writeToGenerator(event, generator); + } catch (Throwable ignored) { + return event.getFormattedMessage(); + } + + sw.append('\n'); + return sw.toString(); + } + + private void writeToGenerator(ILoggingEvent event, JsonGenerator generator) throws IOException { + generator.writeStartObject(); + + generator.writeStringField(ElementName.MESSAGE, event.getFormattedMessage()); + generator.writeNumberField(ElementName.TIMESTAMP, event.getTimeStamp()); + generator.writeStringField(ElementName.LOG_LEVEL, event.getLevel().toString()); + generator.writeStringField(ElementName.LOGGER_NAME, event.getLoggerName()); + generator.writeStringField(ElementName.THREAD_NAME, event.getThreadName()); + + if (event.hasCallerData()) { + StackTraceElement element = event.getCallerData()[event.getCallerData().length - 1]; + generator.writeStringField(ElementName.CLASS_NAME, element.getClassName()); + generator.writeStringField(ElementName.METHOD_NAME, element.getMethodName()); + generator.writeNumberField(ElementName.LINE_NUMBER, element.getLineNumber()); + } + + Map mdcPropertyMap = event.getMDCPropertyMap(); + for (Map.Entry entry : mdcPropertyMap.entrySet()) { + if (entry.getValue() != null && !entry.getValue().isEmpty()) { + if (entry.getKey().startsWith(NEW_RELIC_PREFIX)) { + String key = entry.getKey().substring(NEW_RELIC_PREFIX.length()); + generator.writeStringField(key, entry.getValue()); + } else if (LogExtensionConfig.shouldAddMDC()) { + generator.writeStringField(CONTEXT_PREFIX + entry.getKey(), entry.getValue()); + } + } + } + + if (event.getMarkerList() != null && !event.getMarkerList().isEmpty()) { + generator.writeArrayFieldStart(ElementName.MARKER); + for (Marker marker : event.getMarkerList()) { + generator.writeString(marker.getName()); + } + generator.writeEndArray(); + + } + + Object[] customArgumentArray = event.getArgumentArray(); + if (customArgumentArray != null) { + for (Object oneCustomArgumentObject : customArgumentArray) { + if (oneCustomArgumentObject instanceof CustomArgument) { + CustomArgument customArgument = (CustomArgument) oneCustomArgumentObject; + generator.writeStringField(customArgument.getKey(), customArgument.getValue()); + } + } + } + + IThrowableProxy proxy = event.getThrowableProxy(); + if (proxy != null) { + generator.writeObjectField(ElementName.ERROR_CLASS, proxy.getClassName()); + generator.writeObjectField(ElementName.ERROR_MESSAGE, proxy.getMessage()); + generator.writeObjectField(ElementName.ERROR_STACK, ExceptionUtil.transformLogbackStackTraceString(ThrowableProxyUtil.asString(proxy))); + } + + generator.writeEndObject(); + } + + @Override + public String getFileHeader() { + return null; + } + + @Override + public String getPresentationHeader() { + return null; + } + + @Override + public String getPresentationFooter() { + return null; + } + + @Override + public String getFileFooter() { + return null; + } + + @Override + public String getContentType() { + return "application/json"; + } + + // + @Override + public void setContext(Context context) { + this.context = context; + } + + // + @Override + public Context getContext() { + return context; + } + + // + @Override + public void addStatus(Status status) { + context.getStatusManager().add(status); + } + + // + @Override + public void addInfo(String msg) { + if (context != null) { + context.getStatusManager().add(new InfoStatus(msg, this)); + } + } + + // + @Override + public void addInfo(String msg, Throwable ex) { + if (context != null) { + context.getStatusManager().add(new InfoStatus(msg, this, ex)); + } + } + + // + @Override + public void addWarn(String msg) { + if (context != null) { + context.getStatusManager().add(new WarnStatus(msg, this)); + } + } + + // + @Override + public void addWarn(String msg, Throwable ex) { + if (context != null) { + context.getStatusManager().add(new WarnStatus(msg, this, ex)); + } + } + + // + @Override + public void addError(String msg) { + if (context != null) { + context.getStatusManager().add(new ErrorStatus(msg, this)); + } + } + + // + @Override + public void addError(String msg, Throwable ex) { + if (context != null) { + context.getStatusManager().add(new ErrorStatus(msg, this, ex)); + } + } + + @Override + public void start() { + started = true; + } + + @Override + public void stop() { + started = false; + } + + @Override + public boolean isStarted() { + return started; + } +} diff --git a/logback13/src/test/java/JsonFactoryProviderTest.java b/logback13/src/test/java/JsonFactoryProviderTest.java new file mode 100644 index 0000000..b7b1d04 --- /dev/null +++ b/logback13/src/test/java/JsonFactoryProviderTest.java @@ -0,0 +1,33 @@ +import com.fasterxml.jackson.core.JsonFactory; +import com.newrelic.logging.logback13.JsonFactoryProvider; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +class JsonFactoryProviderTest { + + @Test + void shouldAlwaysGetSameInstance() { + JsonFactory instance1 = JsonFactoryProvider.getInstance(); + JsonFactory instance2 = JsonFactoryProvider.getInstance(); + + assertNotNull(instance1, "Instance 1 should not be null"); + assertNotNull(instance2, "Instance 2 should not be null"); + assertSame(instance1, instance2, "Instances should be the same"); + } + + @Test + void shouldReturnNonNullJsonFactoryInstance() { + JsonFactory jsonFactory = JsonFactoryProvider.getInstance(); + assertNotNull(jsonFactory, "JsonFactory instance should not be null"); + } + + @Test + void shouldReturnSameJsonFactoryInstance() { + JsonFactory firstInstance = JsonFactoryProvider.getInstance(); + JsonFactory secondInstance = JsonFactoryProvider.getInstance(); + assertSame(firstInstance, secondInstance, "Both instances should be the same"); + } + +} diff --git a/logback13/src/test/java/NewRelicJsonLayoutTest.java b/logback13/src/test/java/NewRelicJsonLayoutTest.java new file mode 100644 index 0000000..8283129 --- /dev/null +++ b/logback13/src/test/java/NewRelicJsonLayoutTest.java @@ -0,0 +1,39 @@ +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.core.status.Status; +import ch.qos.logback.core.status.StatusManager; +import com.newrelic.logging.logback13.NewRelicJsonLayout; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +public class NewRelicJsonLayoutTest { + private NewRelicJsonLayout layout; + private LoggerContext mockContext; + private StatusManager mockStatusManager; + + @BeforeEach + void setUp() { + layout = new NewRelicJsonLayout(); + mockContext = mock(LoggerContext.class); + layout.setContext(mockContext); + mockStatusManager = mock(StatusManager.class); + when(mockContext.getStatusManager()).thenReturn(mockStatusManager); + } + + @Test + void testContextIsSet() { + assertNotNull(layout.getContext(), "Context should not be null"); + assertEquals(mockContext, layout.getContext(), "Context should match the one set in the test"); + } + + @Test + void testLoggingMethodsWithoutNullPointer() { + layout.addInfo("Test Info Message"); + layout.addWarn("Test Warn Message"); + layout.addError("Test Error Message"); + + verify(mockStatusManager, times(3)).add(any(Status.class)); + } +} diff --git a/logback13/src/test/java/NewRelicLogbackTests.java b/logback13/src/test/java/NewRelicLogbackTests.java new file mode 100644 index 0000000..e9f0e2f --- /dev/null +++ b/logback13/src/test/java/NewRelicLogbackTests.java @@ -0,0 +1,324 @@ +/* + * Copyright 2025. New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import ch.qos.logback.classic.AsyncAppender; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.classic.spi.ThrowableProxy; +import ch.qos.logback.core.ConsoleAppender; +import com.google.common.collect.ImmutableMap; +import com.newrelic.api.agent.Agent; +import com.newrelic.logging.core.LogAsserts; +import com.newrelic.logging.logback13.CustomArgument; +import com.newrelic.logging.logback13.NewRelicAsyncAppender; +import com.newrelic.logging.logback13.NewRelicEncoder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.mockito.Mockito; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.function.Supplier; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.*; + +class NewRelicLogbackTests { + private AsyncAppender appender; + private LoggingEvent event; + private PipedOutputStream outputStream; + private BufferedReader bufferedReader; + private String output; + + private static Supplier savedSupplier; + private boolean isNoOpMDC; + + @BeforeEach + void setUp() throws Exception { + // Clear MDC data before each test + MDC.clear(); + isNoOpMDC = NewRelicAsyncAppender.IsNoOpMDCHolder.isNoOpMDC; + outputStream = new PipedOutputStream(); + PipedInputStream inputStream = new PipedInputStream(outputStream); + bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + } + + @AfterEach + void tearDown() throws Exception { + // Clear MDC data before each test + MDC.clear(); + NewRelicAsyncAppender.IsNoOpMDCHolder.isNoOpMDC = isNoOpMDC; + appender.stop(); + appender.detachAndStopAllAppenders(); + outputStream.close(); + bufferedReader.close(); + } + + @BeforeAll + static void setUpClass() { + savedSupplier = NewRelicAsyncAppender.agentSupplier; + } + + @AfterAll + static void tearDownClass() { + NewRelicAsyncAppender.agentSupplier = savedSupplier; + } + + @Test + @Timeout(3) + void shouldWrapJsonConsoleAppenderCorrectly() throws Throwable { + givenMockAgentData(); + givenARedirectedAppender(); + givenALoggingEvent(); + whenTheEventIsAppended(); + thenMockAgentDataIsInTheMessage(); + thenJsonLayoutWasUsed(); + } + + @Test + @Timeout(3) + void shouldAllWorkCorrectlyEvenWithoutMDC() throws Throwable { + givenMockAgentData(); + givenARedirectedAppender(); + givenALoggingEventWithMDCDisabled(); + givenMDCIsANoOp(); + whenTheEventIsAppended(); + thenMockAgentDataIsInTheMessage(); + thenJsonLayoutWasUsed(); + } + + @Test + @Timeout(3) + void shouldAppendCallerDataToJsonCorrectly() throws Throwable { + givenMockAgentData(); + givenARedirectedAppender(); + givenALoggingEventWithCallerData(); + whenTheEventIsAppended(); + thenJsonLayoutWasUsed(); + thenTheCallerDataIsInTheMessage(); + } + + @Test + @Timeout(3) + void shouldAppendErrorDataCorrectly() throws Throwable { + givenMockAgentData(); + givenARedirectedAppender(); + givenALoggingEventWithExceptionData(); + whenTheEventIsAppended(); + thenJsonLayoutWasUsed(); + thenTheExceptionDataIsInTheMessage(); + } + + @Test + @Timeout(3) + void shouldAppendCustomArgsToJsonCorrectly() throws Throwable { + givenMockAgentData(); + givenARedirectedAppender(); + givenALoggingEventWithCustomArgs(); + whenTheEventIsAppended(); + thenJsonLayoutWasUsed(); + thenTheCustomArgsAreInTheMessage(); + } + + @Test + @Timeout(3) + void shouldAppendMDCArgsToJsonWhenEnabled() throws Throwable { + givenMockAgentData(); + givenARedirectedAppender(); + givenALoggingEventWithMDCEnabled(); + whenTheEventIsAppended(); + thenJsonLayoutWasUsed(); + thenTheMDCFieldsAreInTheMessage(true); + } + + @Test + @Timeout(3) + void shouldNotAppendMDCArgsToJsonWhenDisabled() throws Throwable { + givenMockAgentData(); + givenARedirectedAppender(); + givenALoggingEventWithMDCDisabled(); + whenTheEventIsAppended(); + thenJsonLayoutWasUsed(); + thenTheMDCFieldsAreInTheMessage(false); + } + + private void givenMockAgentData() { + Agent mockAgent = Mockito.mock(Agent.class); + Mockito.when(mockAgent.getLinkingMetadata()).thenReturn(ImmutableMap.of("some.key", "some.value")); + NewRelicAsyncAppender.agentSupplier = () -> mockAgent; + } + + private void givenMDCIsANoOp() { + com.newrelic.logging.logback13.NewRelicAsyncAppender.IsNoOpMDCHolder.isNoOpMDC = true; + // Wipe the MDC to mimic a NOPMDCAdapter. + event.setMDCPropertyMap(new HashMap<>()); + } + + private void givenALoggingEvent() { + event = new LoggingEvent(); + event.setMessage("test_error_message"); + event.setLevel(Level.ERROR); + event.setLoggerContext((LoggerContext) LoggerFactory.getILoggerFactory()); + } + + private void givenALoggingEventWithExceptionData() { + givenALoggingEvent(); + event.setThrowableProxy(new ThrowableProxy(new Exception("~~ oops ~~"))); + } + + private void givenALoggingEventWithCallerData() { + givenALoggingEvent(); + event.setCallerData(new StackTraceElement[] { new Exception().getStackTrace()[0] }); + } + + private void givenALoggingEventWithCustomArgs() { + givenALoggingEvent(); + CustomArgument customArgument1 = new CustomArgument("customKey1", "customValue1"); + CustomArgument customArgument2 = new CustomArgument("customKey2", "customValue2"); + Object[] customArgs = new Object[2]; + customArgs[0] = customArgument1; + customArgs[1] = customArgument2; + event.setArgumentArray(customArgs); + } + + private void givenALoggingEventWithMDCEnabled() { + // Enable MDC collection + System.setProperty("newrelic.log_extension.add_mdc", "true"); + + // Add MDC data + MDC.put("contextKey1", "contextData1"); + MDC.put("contextKey2", "contextData2"); + MDC.put("contextKey3", "contextData3"); + + givenALoggingEvent(); + } + + private void givenALoggingEventWithMDCDisabled() { + // Disable MDC collection + System.setProperty("newrelic.log_extension.add_mdc", "false"); + + // Add MDC data + MDC.put("contextKey1", "contextData1"); + MDC.put("contextKey2", "contextData2"); + MDC.put("contextKey3", "contextData3"); + + givenALoggingEvent(); + } + + private void givenARedirectedAppender() { + NewRelicEncoder encoder = new NewRelicEncoder(); + encoder.start(); + + LoggerContext context = new LoggerContext(); + ConsoleAppender consoleAppender = new ConsoleAppender<>(); + consoleAppender.setContext(context); + consoleAppender.setEncoder(encoder); + consoleAppender.start(); + // must be set _after_ start() + consoleAppender.setOutputStream(outputStream); + + appender = new NewRelicAsyncAppender(); + appender.setContext(context); + appender.addAppender(consoleAppender); + appender.start(); + } + + private void whenTheEventIsAppended() { + appender.doAppend(event); + } + + private void thenJsonLayoutWasUsed() throws IOException { + LogAsserts.assertFieldValues( + getOutput(), + ImmutableMap.of( + "message", "test_error_message", + "log.level", "ERROR", + "some.key", "some.value" + ) + ); + } + + private void thenMockAgentDataIsInTheMessage() throws Throwable { + assertTrue( + getOutput().contains("some.key=some.value") + || getOutput().contains("\"some.key\":\"some.value\""), + "Expected >>" + getOutput() + "<< to contain some.key to some.value" + ); + } + + private void thenTheCallerDataIsInTheMessage() throws Throwable { + LogAsserts.assertFieldValues( + getOutput(), + ImmutableMap.of("class.name", this.getClass().getName(), "method.name", "givenALoggingEventWithCallerData") + ); + } + + private void thenTheExceptionDataIsInTheMessage() throws Throwable { + LogAsserts.assertFieldValues( + getOutput(), + ImmutableMap.of( + "error.class", "java.lang.Exception", + "error.stack", Pattern.compile(".*NewRelicLogbackTests\\.shouldAppendErrorDataCorrectly.*", Pattern.DOTALL), + "error.message", "~~ oops ~~") + ); + } + + private void thenTheCustomArgsAreInTheMessage() throws Throwable { + LogAsserts.assertFieldValues( + getOutput(), + ImmutableMap.of("customKey1", "customValue1", "customKey2", "customValue2") + ); + } + + private void thenTheMDCFieldsAreInTheMessage(boolean shouldExist) throws Throwable { + String result = getOutput(); + boolean contextKey1Exists = LogAsserts.assertFieldExistence( + "context.contextKey1", + result, + shouldExist + ); + assertEquals(shouldExist, contextKey1Exists, "MDC context.contextKey1 should exist: " + shouldExist); + + boolean contextKey2Exists = LogAsserts.assertFieldExistence( + "context.contextKey2", + result, + shouldExist + ); + assertEquals(shouldExist, contextKey2Exists, "MDC context.contextKey2 should exist: " + shouldExist); + + boolean contextKey3Exists = LogAsserts.assertFieldExistence( + "context.contextKey3", + result, + shouldExist + ); + assertEquals(shouldExist, contextKey3Exists, "MDC context.contextKey3 should exist: " + shouldExist); + } + + private String getOutput() throws IOException { + if (output == null) { + output = bufferedReader.readLine() + "\n"; + } + assertNotNull(output); + return output; + } + + + + + +} From b6e7c75bbdb01a8b9c1d2112fa4b57693462182f Mon Sep 17 00:00:00 2001 From: edeleon Date: Mon, 26 May 2025 18:10:58 -0700 Subject: [PATCH 03/41] add logback-1.3 plugin --- logback13/build.gradle.kts | 8 +-- logback13/spotbugs-filter.xml | 64 +++++++++---------- .../logback13/NewRelicAsyncAppender.java | 32 ++++------ .../logging/logback13/NewRelicEncoder.java | 9 +-- .../logging/logback13/NewRelicJsonLayout.java | 5 +- ...Tests.java => NewRelicLogback13Tests.java} | 42 +++++++----- 6 files changed, 77 insertions(+), 83 deletions(-) rename logback13/src/test/java/{NewRelicLogbackTests.java => NewRelicLogback13Tests.java} (90%) diff --git a/logback13/build.gradle.kts b/logback13/build.gradle.kts index 6fd5eab..9e9789e 100644 --- a/logback13/build.gradle.kts +++ b/logback13/build.gradle.kts @@ -6,7 +6,7 @@ plugins { group = "com.newrelic.logging" // -Prelease=true will render a non-snapshot version -// All other values (include unset) will render a snapshot version. +// All other values (including unset) will render a snapshot version. val release: String? by project val releaseVersion: String by project version = releaseVersion + if ("true" == release) "" else "-SNAPSHOT" @@ -21,16 +21,12 @@ includeInJar.exclude(group = "org.apache.commons") configurations["compileOnly"].extendsFrom(includeInJar) dependencies { - implementation("org.slf4j:slf4j-api:2.0.7") implementation("com.fasterxml.jackson.core:jackson-core:2.11.1") implementation("ch.qos.logback:logback-core:1.3.15") implementation("ch.qos.logback:logback-classic:1.3.15") implementation("com.newrelic.agent.java:newrelic-api:7.6.0") - includeInJar(project(":core")) - testImplementation("ch.qos.logback:logback-core:1.3.15"); - testImplementation("ch.qos.logback:logback-classic:1.3.15"); testImplementation("org.junit.jupiter:junit-jupiter:5.6.2") testImplementation("com.google.guava:guava:30.0-jre") testImplementation("org.mockito:mockito-core:3.4.4") @@ -66,7 +62,7 @@ tasks.register("javadocJar") { archiveClassifier.set("javadoc") } -//apply(from = "$rootDir/gradle/publisher.gradle.kts") +apply(from = "$rootDir/gradle/publish.gradle.kts") tasks.withType { excludeFilter.set(file("spotbugs-filter.xml")) diff --git a/logback13/spotbugs-filter.xml b/logback13/spotbugs-filter.xml index 123384c..da8ce29 100644 --- a/logback13/spotbugs-filter.xml +++ b/logback13/spotbugs-filter.xml @@ -21,39 +21,39 @@ - - - - - + + + + + + + + - - - - - + + + + + + + + - - - - - + + + + + + + + - - - - - + + + + + + + + \ No newline at end of file diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java index ac62135..6118591 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java @@ -2,16 +2,17 @@ * Copyright 2025 New Relic Corporation. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ - package com.newrelic.logging.logback13; import ch.qos.logback.classic.AsyncAppender; import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.LoggingEvent; import com.newrelic.api.agent.Agent; import com.newrelic.api.agent.NewRelic; import org.slf4j.MDC; import org.slf4j.helpers.NOPMDCAdapter; +import java.util.HashMap; import java.util.Map; import java.util.function.Supplier; @@ -48,32 +49,27 @@ protected void preprocess(ILoggingEvent eventObject) { Map linkingMetadata = agentSupplier.get().getLinkingMetadata(); // NR linking metadata is added to the MDC map, if MDC is enabled - if (!IsNoOpMDCHolder.isNoOpMDC) { + if (!isNoOpMDC) { for (Map.Entry entry : linkingMetadata.entrySet()) { MDC.put(NEW_RELIC_PREFIX + entry.getKey(), entry.getValue()); } - super.preprocess(eventObject); - } else { + } + super.preprocess(eventObject); + /* + * In logback-1.3.x, calling eventObject.getMDCPropertyMap() returns an immutable map (Collections.emptyMap()). + * To add New Relic linking metadata to the event, we first check if MDC is disabled (isNoOpMDC). Then we create + * a new MDC map (that is mutable) and add the linking metadata and call setMDCPropertyMap() to set the new map. + */ + if (isNoOpMDC) { for (Map.Entry entry : linkingMetadata.entrySet()) { - /* - * This only works if there is at least one entry in the MDC map. If the MDC map is empty when - * calling eventObject.getMDCPropertyMap() it simply returns Collections.emptyMap() which is - * immutable. Calling put() on the immutable map causes a java.lang.UnsupportedOperationException - * which results in the NR linking metadata never being added. - */ eventObject.getMDCPropertyMap().put(NEW_RELIC_PREFIX + entry.getKey(), entry.getValue()); } } } - /* - Due to issues with simultaneous classloading of the NRAsyncAppender and the Logback classes, - the isNoOpMDC field is wrapped to load it lazily. Visible for testing. - */ - public static class IsNoOpMDCHolder { - public static boolean isNoOpMDC = MDC.getMDCAdapter() instanceof NOPMDCAdapter; - } - //visible for testing public static Supplier agentSupplier = NewRelic::getAgent; + @SuppressWarnings("WeakerAccess") //visible for testing + public static boolean isNoOpMDC = MDC.getMDCAdapter() instanceof NOPMDCAdapter; } + diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java index e608abe..cd70884 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java @@ -38,15 +38,10 @@ public class NewRelicEncoder extends EncoderBase { @Override public byte[] encode(ILoggingEvent event) { String laidOutResult = layout.doLayout(event); - ByteBuffer results = StandardCharsets.UTF_8.encode(laidOutResult); - byte[] resultArray = results.array(); - int i = resultArray.length - 1; - while (i > 0 && resultArray[i - 1] == '\0') { - i--; - } - return Arrays.copyOfRange(resultArray, 0, i); + return laidOutResult.getBytes(StandardCharsets.UTF_8); } + @Override public void start() { super.start(); diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java index 1dc609d..4cf99ee 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java @@ -48,11 +48,10 @@ public String doLayout(ILoggingEvent event) { try (JsonGenerator generator = JsonFactoryProvider.getInstance().createGenerator(sw)) { writeToGenerator(event, generator); } catch (Throwable ignored) { - return event.getFormattedMessage(); + return event.getFormattedMessage() + "\n"; } - sw.append('\n'); - return sw.toString(); + return sw.toString() + "\n"; } private void writeToGenerator(ILoggingEvent event, JsonGenerator generator) throws IOException { diff --git a/logback13/src/test/java/NewRelicLogbackTests.java b/logback13/src/test/java/NewRelicLogback13Tests.java similarity index 90% rename from logback13/src/test/java/NewRelicLogbackTests.java rename to logback13/src/test/java/NewRelicLogback13Tests.java index e9f0e2f..7c7d4f0 100644 --- a/logback13/src/test/java/NewRelicLogbackTests.java +++ b/logback13/src/test/java/NewRelicLogback13Tests.java @@ -32,13 +32,12 @@ import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.nio.charset.StandardCharsets; -import java.util.HashMap; import java.util.function.Supplier; import java.util.regex.Pattern; import static org.junit.jupiter.api.Assertions.*; -class NewRelicLogbackTests { +class NewRelicLogback13Tests { private AsyncAppender appender; private LoggingEvent event; private PipedOutputStream outputStream; @@ -51,8 +50,8 @@ class NewRelicLogbackTests { @BeforeEach void setUp() throws Exception { // Clear MDC data before each test + isNoOpMDC = NewRelicAsyncAppender.isNoOpMDC; MDC.clear(); - isNoOpMDC = NewRelicAsyncAppender.IsNoOpMDCHolder.isNoOpMDC; outputStream = new PipedOutputStream(); PipedInputStream inputStream = new PipedInputStream(outputStream); bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); @@ -62,7 +61,7 @@ void setUp() throws Exception { void tearDown() throws Exception { // Clear MDC data before each test MDC.clear(); - NewRelicAsyncAppender.IsNoOpMDCHolder.isNoOpMDC = isNoOpMDC; + NewRelicAsyncAppender.isNoOpMDC = isNoOpMDC; appender.stop(); appender.detachAndStopAllAppenders(); outputStream.close(); @@ -95,8 +94,8 @@ void shouldWrapJsonConsoleAppenderCorrectly() throws Throwable { void shouldAllWorkCorrectlyEvenWithoutMDC() throws Throwable { givenMockAgentData(); givenARedirectedAppender(); - givenALoggingEventWithMDCDisabled(); givenMDCIsANoOp(); + givenALoggingEvent(); whenTheEventIsAppended(); thenMockAgentDataIsInTheMessage(); thenJsonLayoutWasUsed(); @@ -164,9 +163,8 @@ private void givenMockAgentData() { } private void givenMDCIsANoOp() { - com.newrelic.logging.logback13.NewRelicAsyncAppender.IsNoOpMDCHolder.isNoOpMDC = true; // Wipe the MDC to mimic a NOPMDCAdapter. - event.setMDCPropertyMap(new HashMap<>()); + NewRelicAsyncAppender.isNoOpMDC = true; } private void givenALoggingEvent() { @@ -238,8 +236,13 @@ private void givenARedirectedAppender() { appender.start(); } - private void whenTheEventIsAppended() { + private void whenTheEventIsAppended() throws IOException { appender.doAppend(event); + outputStream.flush(); + } + + private boolean appenderIsIdle() { + return ((NewRelicAsyncAppender) appender).getQueueSize() == 0; } private void thenJsonLayoutWasUsed() throws IOException { @@ -254,10 +257,17 @@ private void thenJsonLayoutWasUsed() throws IOException { } private void thenMockAgentDataIsInTheMessage() throws Throwable { + String output = getOutput(); + + System.out.println("ED: OUTPUT CHARS: "); + for (char c : output.toCharArray()) { + System.out.print((int) c + " "); + } + assertTrue( - getOutput().contains("some.key=some.value") - || getOutput().contains("\"some.key\":\"some.value\""), - "Expected >>" + getOutput() + "<< to contain some.key to some.value" + output.contains("\"some.key\"=\"some.value\"") + || output.contains("\"some.key\":\"some.value\""), + "Expected log output to contain linking metadata: " + output ); } @@ -273,7 +283,7 @@ private void thenTheExceptionDataIsInTheMessage() throws Throwable { getOutput(), ImmutableMap.of( "error.class", "java.lang.Exception", - "error.stack", Pattern.compile(".*NewRelicLogbackTests\\.shouldAppendErrorDataCorrectly.*", Pattern.DOTALL), + "error.stack", Pattern.compile(".*NewRelicLogback13Tests\\.shouldAppendErrorDataCorrectly.*", Pattern.DOTALL), "error.message", "~~ oops ~~") ); } @@ -312,13 +322,11 @@ private void thenTheMDCFieldsAreInTheMessage(boolean shouldExist) throws Throwab private String getOutput() throws IOException { if (output == null) { output = bufferedReader.readLine() + "\n"; + System.out.println("Output: " + output); + appender.stop(); } - assertNotNull(output); +// assertNotNull(output); return output; } - - - - } From 84b3c48f6555b1ddc258f3284b948c713c72a3bd Mon Sep 17 00:00:00 2001 From: edeleon Date: Mon, 26 May 2025 19:07:44 -0700 Subject: [PATCH 04/41] add linking metadata to the argument array when MDC is disabled --- .../logback13/NewRelicAsyncAppender.java | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java index 6118591..b84996b 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java @@ -48,28 +48,38 @@ public void start() { protected void preprocess(ILoggingEvent eventObject) { Map linkingMetadata = agentSupplier.get().getLinkingMetadata(); - // NR linking metadata is added to the MDC map, if MDC is enabled if (!isNoOpMDC) { for (Map.Entry entry : linkingMetadata.entrySet()) { MDC.put(NEW_RELIC_PREFIX + entry.getKey(), entry.getValue()); } + super.preprocess(eventObject); + /* + * In logback-1.3.x, calling eventObject.getMDCPropertyMap() returns an immutable map (Collections.emptyMap()). + * To add New Relic linking metadata to the event, we need to set the argument array with the linking metadata. + * This allows us to maintain compatibility with logback-1.3.x while still supporting the New Relic linking metadata + * in the event object. + */ } - super.preprocess(eventObject); - /* - * In logback-1.3.x, calling eventObject.getMDCPropertyMap() returns an immutable map (Collections.emptyMap()). - * To add New Relic linking metadata to the event, we first check if MDC is disabled (isNoOpMDC). Then we create - * a new MDC map (that is mutable) and add the linking metadata and call setMDCPropertyMap() to set the new map. - */ + if (isNoOpMDC) { + Object[] args = linkingMetadata.entrySet().stream() + .map(event -> CustomArgument.keyValue(event.getKey(), event.getValue())) + .toArray(CustomArgument[]::new); + ((LoggingEvent) eventObject).setArgumentArray(args); + + Map mdcMap = new HashMap<>(); for (Map.Entry entry : linkingMetadata.entrySet()) { - eventObject.getMDCPropertyMap().put(NEW_RELIC_PREFIX + entry.getKey(), entry.getValue()); + mdcMap.put(NEW_RELIC_PREFIX + entry.getKey(), entry.getValue()); } + ((LoggingEvent) eventObject).setMDCPropertyMap(mdcMap); } } //visible for testing public static Supplier agentSupplier = NewRelic::getAgent; - @SuppressWarnings("WeakerAccess") //visible for testing + + // visible for testing + @SuppressWarnings("WeakerAccess") public static boolean isNoOpMDC = MDC.getMDCAdapter() instanceof NOPMDCAdapter; } From bcf0c56fe8444dc478fac9ac8c19e00375404b04 Mon Sep 17 00:00:00 2001 From: edeleon Date: Thu, 29 May 2025 08:19:18 -0700 Subject: [PATCH 05/41] update plugin --- logback13/build.gradle.kts | 4 +- .../logback13/NewRelicAsyncAppender.java | 17 +++++ .../logging/logback13/NewRelicEncoder.java | 12 +--- .../logging/logback13/NewRelicJsonLayout.java | 72 +++++++------------ .../src/test/java/NewRelicLogback13Tests.java | 24 ++----- 5 files changed, 54 insertions(+), 75 deletions(-) diff --git a/logback13/build.gradle.kts b/logback13/build.gradle.kts index 9e9789e..59d813e 100644 --- a/logback13/build.gradle.kts +++ b/logback13/build.gradle.kts @@ -48,8 +48,8 @@ tasks.withType { } configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } tasks.register("sourcesJar") { diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java index b84996b..b2dd26b 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java @@ -44,6 +44,17 @@ public void start() { super.start(); } + @Override + protected void append(ILoggingEvent eventObject) { + Map linkingMetadata = agentSupplier.get().getLinkingMetadata(); + if (!isNoOpMDC) { + for (Map.Entry entry : linkingMetadata.entrySet()) { + MDC.put(NEW_RELIC_PREFIX + entry.getKey(), entry.getValue()); + } + } + super.append(eventObject); + } + @Override protected void preprocess(ILoggingEvent eventObject) { Map linkingMetadata = agentSupplier.get().getLinkingMetadata(); @@ -52,6 +63,12 @@ protected void preprocess(ILoggingEvent eventObject) { for (Map.Entry entry : linkingMetadata.entrySet()) { MDC.put(NEW_RELIC_PREFIX + entry.getKey(), entry.getValue()); } + + Map mdcCopy = MDC.getCopyOfContextMap(); + if (mdcCopy != null && eventObject instanceof LoggingEvent) { + ((LoggingEvent) eventObject).setMDCPropertyMap(mdcCopy); + } + super.preprocess(eventObject); /* * In logback-1.3.x, calling eventObject.getMDCPropertyMap() returns an immutable map (Collections.emptyMap()). diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java index cd70884..da6e215 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java @@ -9,6 +9,8 @@ import ch.qos.logback.core.encoder.Encoder; import ch.qos.logback.core.encoder.EncoderBase; +import java.io.IOException; +import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -31,9 +33,7 @@ * @see Logback Encoders */ public class NewRelicEncoder extends EncoderBase { - private NewRelicJsonLayout layout; - - private Integer maxStackSize = getMaxStackSize(); + private NewRelicJsonLayout layout = new NewRelicJsonLayout(); @Override public byte[] encode(ILoggingEvent event) { @@ -41,18 +41,12 @@ public byte[] encode(ILoggingEvent event) { return laidOutResult.getBytes(StandardCharsets.UTF_8); } - @Override public void start() { super.start(); - layout = new NewRelicJsonLayout(maxStackSize); layout.start(); } - public void setMaxStackSize(final Integer maxStackSize) { - this.maxStackSize = maxStackSize; - } - @Override public byte[] headerBytes() { return new byte[0]; diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java index 4cf99ee..6684e6f 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java @@ -6,57 +6,48 @@ package com.newrelic.logging.logback13; import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.classic.spi.IThrowableProxy; -import ch.qos.logback.classic.spi.ThrowableProxyUtil; import ch.qos.logback.core.Context; -import ch.qos.logback.core.Layout; +import ch.qos.logback.core.LayoutBase; import ch.qos.logback.core.status.ErrorStatus; import ch.qos.logback.core.status.InfoStatus; import ch.qos.logback.core.status.Status; import ch.qos.logback.core.status.WarnStatus; import com.fasterxml.jackson.core.JsonGenerator; import com.newrelic.logging.core.ElementName; -import com.newrelic.logging.core.ExceptionUtil; import com.newrelic.logging.core.LogExtensionConfig; +import com.fasterxml.jackson.core.JsonFactory; import org.slf4j.Marker; import java.io.IOException; import java.io.StringWriter; +import java.util.List; import java.util.Map; import static com.newrelic.logging.core.LogExtensionConfig.CONTEXT_PREFIX; -import static com.newrelic.logging.core.LogExtensionConfig.getMaxStackSize; import static com.newrelic.logging.logback13.NewRelicAsyncAppender.NEW_RELIC_PREFIX; -public class NewRelicJsonLayout implements Layout { - private final Integer maxStackSize; +public class NewRelicJsonLayout extends LayoutBase { private boolean started = false; private Context context; - public NewRelicJsonLayout() { - this(getMaxStackSize()); - } - - public NewRelicJsonLayout(Integer maxStackSize) { - this.maxStackSize = maxStackSize; - } - @Override public String doLayout(ILoggingEvent event) { StringWriter sw = new StringWriter(); - try (JsonGenerator generator = JsonFactoryProvider.getInstance().createGenerator(sw)) { + try (JsonGenerator generator = new JsonFactory().createGenerator(sw);) { writeToGenerator(event, generator); } catch (Throwable ignored) { - return event.getFormattedMessage() + "\n"; + return event.getFormattedMessage(); } - return sw.toString() + "\n"; + sw.append('\n'); + return sw.toString(); } private void writeToGenerator(ILoggingEvent event, JsonGenerator generator) throws IOException { - generator.writeStartObject(); + System.out.println("[writeToGenerator] Executing layout event for: " + event.getFormattedMessage()); + generator.writeStartObject(); generator.writeStringField(ElementName.MESSAGE, event.getFormattedMessage()); generator.writeNumberField(ElementName.TIMESTAMP, event.getTimeStamp()); generator.writeStringField(ElementName.LOG_LEVEL, event.getLevel().toString()); @@ -71,41 +62,30 @@ private void writeToGenerator(ILoggingEvent event, JsonGenerator generator) thro } Map mdcPropertyMap = event.getMDCPropertyMap(); - for (Map.Entry entry : mdcPropertyMap.entrySet()) { - if (entry.getValue() != null && !entry.getValue().isEmpty()) { - if (entry.getKey().startsWith(NEW_RELIC_PREFIX)) { - String key = entry.getKey().substring(NEW_RELIC_PREFIX.length()); - generator.writeStringField(key, entry.getValue()); - } else if (LogExtensionConfig.shouldAddMDC()) { - generator.writeStringField(CONTEXT_PREFIX + entry.getKey(), entry.getValue()); + if (mdcPropertyMap != null) { + System.out.println("[writeToGenerator] mdcPropertyMap is not null, size: " + mdcPropertyMap.size()); + for (Map.Entry entry : mdcPropertyMap.entrySet()) { + if (entry.getValue() != null && !entry.getValue().isEmpty()) { + System.out.println("[writeToGenerator] Current entry key: " + entry.getKey()); + if (entry.getKey().startsWith(NEW_RELIC_PREFIX)) { + String key = entry.getKey().substring(NEW_RELIC_PREFIX.length()); + generator.writeStringField(key, entry.getValue()); + } else if (LogExtensionConfig.shouldAddMDC()){ + generator.writeStringField(CONTEXT_PREFIX + entry.getKey(), entry.getValue()); + } } } + } else { + System.out.println("[writeToGenerator] mdcPropertyMap is null"); } - if (event.getMarkerList() != null && !event.getMarkerList().isEmpty()) { + List markerList = event.getMarkerList(); + if (markerList != null && !markerList.isEmpty()) { generator.writeArrayFieldStart(ElementName.MARKER); - for (Marker marker : event.getMarkerList()) { + for (Marker marker : markerList) { generator.writeString(marker.getName()); } generator.writeEndArray(); - - } - - Object[] customArgumentArray = event.getArgumentArray(); - if (customArgumentArray != null) { - for (Object oneCustomArgumentObject : customArgumentArray) { - if (oneCustomArgumentObject instanceof CustomArgument) { - CustomArgument customArgument = (CustomArgument) oneCustomArgumentObject; - generator.writeStringField(customArgument.getKey(), customArgument.getValue()); - } - } - } - - IThrowableProxy proxy = event.getThrowableProxy(); - if (proxy != null) { - generator.writeObjectField(ElementName.ERROR_CLASS, proxy.getClassName()); - generator.writeObjectField(ElementName.ERROR_MESSAGE, proxy.getMessage()); - generator.writeObjectField(ElementName.ERROR_STACK, ExceptionUtil.transformLogbackStackTraceString(ThrowableProxyUtil.asString(proxy))); } generator.writeEndObject(); diff --git a/logback13/src/test/java/NewRelicLogback13Tests.java b/logback13/src/test/java/NewRelicLogback13Tests.java index 7c7d4f0..afa082a 100644 --- a/logback13/src/test/java/NewRelicLogback13Tests.java +++ b/logback13/src/test/java/NewRelicLogback13Tests.java @@ -176,7 +176,7 @@ private void givenALoggingEvent() { private void givenALoggingEventWithExceptionData() { givenALoggingEvent(); - event.setThrowableProxy(new ThrowableProxy(new Exception("~~ oops ~~"))); + event.setThrowableProxy(new ThrowableProxy(new Exception("some interesting info"))); } private void givenALoggingEventWithCallerData() { @@ -241,10 +241,6 @@ private void whenTheEventIsAppended() throws IOException { outputStream.flush(); } - private boolean appenderIsIdle() { - return ((NewRelicAsyncAppender) appender).getQueueSize() == 0; - } - private void thenJsonLayoutWasUsed() throws IOException { LogAsserts.assertFieldValues( getOutput(), @@ -257,17 +253,10 @@ private void thenJsonLayoutWasUsed() throws IOException { } private void thenMockAgentDataIsInTheMessage() throws Throwable { - String output = getOutput(); - - System.out.println("ED: OUTPUT CHARS: "); - for (char c : output.toCharArray()) { - System.out.print((int) c + " "); - } - assertTrue( - output.contains("\"some.key\"=\"some.value\"") - || output.contains("\"some.key\":\"some.value\""), - "Expected log output to contain linking metadata: " + output + getOutput().contains("some.key=some.value") + || getOutput().contains("\"some.key\":\"some.value\""), + "Expected >>" + getOutput() + "<< to contain some.key to some.value" ); } @@ -284,7 +273,7 @@ private void thenTheExceptionDataIsInTheMessage() throws Throwable { ImmutableMap.of( "error.class", "java.lang.Exception", "error.stack", Pattern.compile(".*NewRelicLogback13Tests\\.shouldAppendErrorDataCorrectly.*", Pattern.DOTALL), - "error.message", "~~ oops ~~") + "error.message", "some error message") ); } @@ -322,10 +311,9 @@ private void thenTheMDCFieldsAreInTheMessage(boolean shouldExist) throws Throwab private String getOutput() throws IOException { if (output == null) { output = bufferedReader.readLine() + "\n"; - System.out.println("Output: " + output); appender.stop(); } -// assertNotNull(output); + assertNotNull(output); return output; } From 5bd43ab5b0c53b1e4b55f6ebf900f55f5dd5c173 Mon Sep 17 00:00:00 2001 From: edeleon Date: Thu, 5 Jun 2025 03:20:37 -0700 Subject: [PATCH 06/41] mdc reporting --- logback13/build.gradle.kts | 2 +- .../logback13/CustomLoggingEventWrapper.java | 111 ++++++++++++++++++ .../logback13/NewRelicAsyncAppender.java | 94 ++++++++------- .../logging/logback13/NewRelicEncoder.java | 8 +- .../logging/logback13/NewRelicJsonLayout.java | 68 +++++------ 5 files changed, 196 insertions(+), 87 deletions(-) create mode 100644 logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java diff --git a/logback13/build.gradle.kts b/logback13/build.gradle.kts index 59d813e..4d69045 100644 --- a/logback13/build.gradle.kts +++ b/logback13/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - java + id("java") id("com.github.spotbugs").version("4.4.4") } diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java b/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java new file mode 100644 index 0000000..4e98c3f --- /dev/null +++ b/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java @@ -0,0 +1,111 @@ +package com.newrelic.logging.logback13; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.IThrowableProxy; +import ch.qos.logback.classic.spi.LoggerContextVO; +import org.slf4j.Marker; +import org.slf4j.event.KeyValuePair; + +import java.util.List; +import java.util.Map; + +public class CustomLoggingEventWrapper implements ILoggingEvent { + private final ILoggingEvent delegate; + private final Map customMdc; + + public CustomLoggingEventWrapper(ILoggingEvent delegate, Map mdcOverride) { + this.delegate = delegate; + this.customMdc = mdcOverride; + } + + @Override + public Map getMDCPropertyMap() { + return customMdc; + } + + @Override + public String getThreadName() { + return delegate.getThreadName(); + } + + @Override + public Level getLevel() { + return delegate.getLevel(); + } + + @Override + public String getMessage() { + return delegate.getMessage(); + } + + @Override + public Object[] getArgumentArray() { + return delegate.getArgumentArray(); + } + + @Override + public String getFormattedMessage() { + return delegate.getFormattedMessage(); + } + + @Override + public String getLoggerName() { + return delegate.getLoggerName(); + } + + @Override + public LoggerContextVO getLoggerContextVO() { + return delegate.getLoggerContextVO(); + } + + @Override + public IThrowableProxy getThrowableProxy() { + return delegate.getThrowableProxy(); + } + + @Override + public StackTraceElement[] getCallerData() { + return delegate.getCallerData(); + } + + @Override + public boolean hasCallerData() { + return delegate.hasCallerData(); + } + + @Override + public List getMarkerList() { + return delegate.getMarkerList(); + } + + @Override + public Map getMdc() { + return delegate.getMdc(); + } + + @Override + public long getTimeStamp() { + return delegate.getTimeStamp(); + } + + @Override + public int getNanoseconds() { + return delegate.getNanoseconds(); + } + + @Override + public long getSequenceNumber() { + return delegate.getSequenceNumber(); + } + + @Override + public List getKeyValuePairs() { + return delegate.getKeyValuePairs(); + } + + @Override + public void prepareForDeferredProcessing() { + delegate.prepareForDeferredProcessing(); + } +} diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java index b2dd26b..631acd0 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java @@ -6,7 +6,6 @@ import ch.qos.logback.classic.AsyncAppender; import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.classic.spi.LoggingEvent; import com.newrelic.api.agent.Agent; import com.newrelic.api.agent.NewRelic; import org.slf4j.MDC; @@ -16,6 +15,8 @@ import java.util.Map; import java.util.function.Supplier; +import static com.newrelic.logging.core.LogExtensionConfig.CONTEXT_PREFIX; + /** * An {@link AsyncAppender} implementation that synchronously captures New Relic trace data. *

@@ -45,51 +46,64 @@ public void start() { } @Override - protected void append(ILoggingEvent eventObject) { - Map linkingMetadata = agentSupplier.get().getLinkingMetadata(); - if (!isNoOpMDC) { - for (Map.Entry entry : linkingMetadata.entrySet()) { - MDC.put(NEW_RELIC_PREFIX + entry.getKey(), entry.getValue()); - } - } - super.append(eventObject); + protected void preprocess(ILoggingEvent eventObject) { + eventObject.prepareForDeferredProcessing(); +// Map linkingMetadata = agentSupplier.get().getLinkingMetadata(); +// Map mdcCopy = MDC.getMDCAdapter().getCopyOfContextMap(); +// Map combinedContextMap = new HashMap<>(); +// ILoggingEvent wrappedEvent; +// System.out.println("[NewRelicAsyncAppender.preprocess] linkingMetadata = agentSupplier.get().getLinkingMetadata() : " + linkingMetadata.entrySet()); +// System.out.println("[NewRelicAsyncAppender.preprocess] mdcCopy = eventObject.getMDCPropertyMap() : " + mdcCopy.entrySet()); +// +// if (!isNoOpMDC) { +// combinedContextMap.putAll(linkingMetadata); +// +// for (Map.Entry entry : mdcCopy.entrySet()) { +// combinedContextMap.put(CONTEXT_PREFIX + entry.getKey(), entry.getValue()); +// } +// System.out.println("[NewRelicAsyncAppender.preprocess] combinedContextMap after adding CONTEXT_PREFIX: " + combinedContextMap); +// +// wrappedEvent = new CustomLoggingEventWrapper(eventObject, combinedContextMap); +// super.preprocess(wrappedEvent); +// } +// +// /* +// * In logback-1.3.x, calling eventObject.getMDCPropertyMap() returns an immutable map. +// * To add New Relic linking metadata to the event, we need to pull the existing ContextMap and set it as the MDCPropertyMap. +// * This allows us to maintain compatibility with logback-1.3.x while still supporting the New Relic linking metadata +// * in the event object. +// */ +// if (isNoOpMDC) { +// for (Map.Entry entry : linkingMetadata.entrySet()) { +// combinedContextMap.put(NEW_RELIC_PREFIX + entry.getKey(), entry.getValue()); +// } +// wrappedEvent = new CustomLoggingEventWrapper(eventObject, combinedContextMap); +// super.preprocess(wrappedEvent); +// } } @Override - protected void preprocess(ILoggingEvent eventObject) { - Map linkingMetadata = agentSupplier.get().getLinkingMetadata(); - - if (!isNoOpMDC) { - for (Map.Entry entry : linkingMetadata.entrySet()) { - MDC.put(NEW_RELIC_PREFIX + entry.getKey(), entry.getValue()); - } - - Map mdcCopy = MDC.getCopyOfContextMap(); - if (mdcCopy != null && eventObject instanceof LoggingEvent) { - ((LoggingEvent) eventObject).setMDCPropertyMap(mdcCopy); - } - - super.preprocess(eventObject); - /* - * In logback-1.3.x, calling eventObject.getMDCPropertyMap() returns an immutable map (Collections.emptyMap()). - * To add New Relic linking metadata to the event, we need to set the argument array with the linking metadata. - * This allows us to maintain compatibility with logback-1.3.x while still supporting the New Relic linking metadata - * in the event object. - */ + protected void append(ILoggingEvent eventObject) { + if (!isStarted()) { + return; } - if (isNoOpMDC) { - Object[] args = linkingMetadata.entrySet().stream() - .map(event -> CustomArgument.keyValue(event.getKey(), event.getValue())) - .toArray(CustomArgument[]::new); - ((LoggingEvent) eventObject).setArgumentArray(args); - - Map mdcMap = new HashMap<>(); - for (Map.Entry entry : linkingMetadata.entrySet()) { - mdcMap.put(NEW_RELIC_PREFIX + entry.getKey(), entry.getValue()); - } - ((LoggingEvent) eventObject).setMDCPropertyMap(mdcMap); + Map baseMdc = MDC.getMDCAdapter().getCopyOfContextMap(); + Map combinedContextMap = new HashMap<>(); + + for (Map.Entry entry : baseMdc.entrySet()) { + combinedContextMap.put(CONTEXT_PREFIX + entry.getKey(), entry.getValue()); } + + Map linkingMetadata = agentSupplier.get().getLinkingMetadata(); + combinedContextMap.putAll(linkingMetadata); + System.out.println("[NewRelicAsyncAppender.append] combinedContextMap: " + combinedContextMap.entrySet()); + + ILoggingEvent wrappedEvent = new CustomLoggingEventWrapper(eventObject, combinedContextMap); + + System.out.println("[NewRelicAsyncAppender.append] wrappedEvent: " + wrappedEvent.getMDCPropertyMap().entrySet()); + + super.append(wrappedEvent); } //visible for testing diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java index da6e215..3f3cbbc 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java @@ -9,13 +9,7 @@ import ch.qos.logback.core.encoder.Encoder; import ch.qos.logback.core.encoder.EncoderBase; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.util.Arrays; - -import static com.newrelic.logging.core.LogExtensionConfig.getMaxStackSize; /** * An {@link ch.qos.logback.core.encoder.Encoder} that will write New Relic's JSON format. @@ -33,7 +27,7 @@ * @see Logback Encoders */ public class NewRelicEncoder extends EncoderBase { - private NewRelicJsonLayout layout = new NewRelicJsonLayout(); + private final NewRelicJsonLayout layout = new NewRelicJsonLayout(); @Override public byte[] encode(ILoggingEvent event) { diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java index 6684e6f..433ea15 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java @@ -16,10 +16,15 @@ import com.newrelic.logging.core.ElementName; import com.newrelic.logging.core.LogExtensionConfig; import com.fasterxml.jackson.core.JsonFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; import org.slf4j.Marker; +import org.slf4j.helpers.NOPMDCAdapter; import java.io.IOException; import java.io.StringWriter; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -31,55 +36,50 @@ public class NewRelicJsonLayout extends LayoutBase { private Context context; @Override - public String doLayout(ILoggingEvent event) { + public String doLayout(ILoggingEvent eventObject) { StringWriter sw = new StringWriter(); - try (JsonGenerator generator = new JsonFactory().createGenerator(sw);) { - writeToGenerator(event, generator); - } catch (Throwable ignored) { - return event.getFormattedMessage(); + try { + JsonGenerator generator = new JsonFactory().createGenerator(sw); + writeToGenerator(eventObject, generator); + } catch (IOException ignored) { + return eventObject.getFormattedMessage(); } sw.append('\n'); + System.out.println("[NewRelicJsonLayout.doLayout] sw.toString() : " + sw.toString()); return sw.toString(); } - private void writeToGenerator(ILoggingEvent event, JsonGenerator generator) throws IOException { - System.out.println("[writeToGenerator] Executing layout event for: " + event.getFormattedMessage()); + private void writeToGenerator(ILoggingEvent eventObject, JsonGenerator generator) throws IOException { + boolean isNoOpMDC = MDC.getMDCAdapter() instanceof NOPMDCAdapter; generator.writeStartObject(); - generator.writeStringField(ElementName.MESSAGE, event.getFormattedMessage()); - generator.writeNumberField(ElementName.TIMESTAMP, event.getTimeStamp()); - generator.writeStringField(ElementName.LOG_LEVEL, event.getLevel().toString()); - generator.writeStringField(ElementName.LOGGER_NAME, event.getLoggerName()); - generator.writeStringField(ElementName.THREAD_NAME, event.getThreadName()); - - if (event.hasCallerData()) { - StackTraceElement element = event.getCallerData()[event.getCallerData().length - 1]; + generator.writeStringField(ElementName.MESSAGE, eventObject.getFormattedMessage()); + generator.writeNumberField(ElementName.TIMESTAMP, eventObject.getTimeStamp()); + generator.writeStringField(ElementName.LOG_LEVEL, eventObject.getLevel().toString()); + generator.writeStringField(ElementName.LOGGER_NAME, eventObject.getLoggerName()); + generator.writeStringField(ElementName.THREAD_NAME, eventObject.getThreadName()); + + if (eventObject.hasCallerData()) { + StackTraceElement element = eventObject.getCallerData()[eventObject.getCallerData().length - 1]; generator.writeStringField(ElementName.CLASS_NAME, element.getClassName()); generator.writeStringField(ElementName.METHOD_NAME, element.getMethodName()); generator.writeNumberField(ElementName.LINE_NUMBER, element.getLineNumber()); } - Map mdcPropertyMap = event.getMDCPropertyMap(); + Map mdcPropertyMap = eventObject.getMDCPropertyMap(); if (mdcPropertyMap != null) { - System.out.println("[writeToGenerator] mdcPropertyMap is not null, size: " + mdcPropertyMap.size()); for (Map.Entry entry : mdcPropertyMap.entrySet()) { - if (entry.getValue() != null && !entry.getValue().isEmpty()) { - System.out.println("[writeToGenerator] Current entry key: " + entry.getKey()); - if (entry.getKey().startsWith(NEW_RELIC_PREFIX)) { - String key = entry.getKey().substring(NEW_RELIC_PREFIX.length()); - generator.writeStringField(key, entry.getValue()); - } else if (LogExtensionConfig.shouldAddMDC()){ - generator.writeStringField(CONTEXT_PREFIX + entry.getKey(), entry.getValue()); - } - } + generator.writeStringField(entry.getKey(), entry.getValue()); + } + } else if (!isNoOpMDC) { + for (Map.Entry entry : MDC.getCopyOfContextMap().entrySet()) { + generator.writeStringField(entry.getKey(), entry.getValue()); } - } else { - System.out.println("[writeToGenerator] mdcPropertyMap is null"); } - List markerList = event.getMarkerList(); + List markerList = eventObject.getMarkerList(); if (markerList != null && !markerList.isEmpty()) { generator.writeArrayFieldStart(ElementName.MARKER); for (Marker marker : markerList) { @@ -87,7 +87,6 @@ private void writeToGenerator(ILoggingEvent event, JsonGenerator generator) thro } generator.writeEndArray(); } - generator.writeEndObject(); } @@ -116,25 +115,21 @@ public String getContentType() { return "application/json"; } - // @Override public void setContext(Context context) { this.context = context; } - // @Override public Context getContext() { return context; } - // @Override public void addStatus(Status status) { context.getStatusManager().add(status); } - // @Override public void addInfo(String msg) { if (context != null) { @@ -142,7 +137,6 @@ public void addInfo(String msg) { } } - // @Override public void addInfo(String msg, Throwable ex) { if (context != null) { @@ -150,7 +144,6 @@ public void addInfo(String msg, Throwable ex) { } } - // @Override public void addWarn(String msg) { if (context != null) { @@ -158,7 +151,6 @@ public void addWarn(String msg) { } } - // @Override public void addWarn(String msg, Throwable ex) { if (context != null) { @@ -166,7 +158,6 @@ public void addWarn(String msg, Throwable ex) { } } - // @Override public void addError(String msg) { if (context != null) { @@ -174,7 +165,6 @@ public void addError(String msg) { } } - // @Override public void addError(String msg, Throwable ex) { if (context != null) { From f6e95b1559931a6fb982220fc465c9fd1f073247 Mon Sep 17 00:00:00 2001 From: edeleon Date: Thu, 5 Jun 2025 03:44:14 -0700 Subject: [PATCH 07/41] refactor and clean up --- .../logback13/NewRelicAsyncAppender.java | 66 ++++++------------- 1 file changed, 19 insertions(+), 47 deletions(-) diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java index 631acd0..128c6d7 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java @@ -39,70 +39,42 @@ public class NewRelicAsyncAppender extends AsyncAppender { public static final String NEW_RELIC_PREFIX = "NewRelic:"; - // required for logback-1.3.x compatibility - @Override - public void start() { - super.start(); - } - @Override protected void preprocess(ILoggingEvent eventObject) { eventObject.prepareForDeferredProcessing(); -// Map linkingMetadata = agentSupplier.get().getLinkingMetadata(); -// Map mdcCopy = MDC.getMDCAdapter().getCopyOfContextMap(); -// Map combinedContextMap = new HashMap<>(); -// ILoggingEvent wrappedEvent; -// System.out.println("[NewRelicAsyncAppender.preprocess] linkingMetadata = agentSupplier.get().getLinkingMetadata() : " + linkingMetadata.entrySet()); -// System.out.println("[NewRelicAsyncAppender.preprocess] mdcCopy = eventObject.getMDCPropertyMap() : " + mdcCopy.entrySet()); -// -// if (!isNoOpMDC) { -// combinedContextMap.putAll(linkingMetadata); -// -// for (Map.Entry entry : mdcCopy.entrySet()) { -// combinedContextMap.put(CONTEXT_PREFIX + entry.getKey(), entry.getValue()); -// } -// System.out.println("[NewRelicAsyncAppender.preprocess] combinedContextMap after adding CONTEXT_PREFIX: " + combinedContextMap); -// -// wrappedEvent = new CustomLoggingEventWrapper(eventObject, combinedContextMap); -// super.preprocess(wrappedEvent); -// } -// -// /* -// * In logback-1.3.x, calling eventObject.getMDCPropertyMap() returns an immutable map. -// * To add New Relic linking metadata to the event, we need to pull the existing ContextMap and set it as the MDCPropertyMap. -// * This allows us to maintain compatibility with logback-1.3.x while still supporting the New Relic linking metadata -// * in the event object. -// */ -// if (isNoOpMDC) { -// for (Map.Entry entry : linkingMetadata.entrySet()) { -// combinedContextMap.put(NEW_RELIC_PREFIX + entry.getKey(), entry.getValue()); -// } -// wrappedEvent = new CustomLoggingEventWrapper(eventObject, combinedContextMap); -// super.preprocess(wrappedEvent); -// } } @Override protected void append(ILoggingEvent eventObject) { + ILoggingEvent wrappedEvent; if (!isStarted()) { return; } - Map baseMdc = MDC.getMDCAdapter().getCopyOfContextMap(); Map combinedContextMap = new HashMap<>(); - for (Map.Entry entry : baseMdc.entrySet()) { - combinedContextMap.put(CONTEXT_PREFIX + entry.getKey(), entry.getValue()); + Map copyMdc = MDC.getMDCAdapter().getCopyOfContextMap(); + if (copyMdc != null) { + for (Map.Entry entry : copyMdc.entrySet()) { + if (entry.getKey() != null && entry.getValue() != null) { + combinedContextMap.put(CONTEXT_PREFIX + entry.getKey(), entry.getValue()); + } + } } Map linkingMetadata = agentSupplier.get().getLinkingMetadata(); - combinedContextMap.putAll(linkingMetadata); - System.out.println("[NewRelicAsyncAppender.append] combinedContextMap: " + combinedContextMap.entrySet()); - - ILoggingEvent wrappedEvent = new CustomLoggingEventWrapper(eventObject, combinedContextMap); - - System.out.println("[NewRelicAsyncAppender.append] wrappedEvent: " + wrappedEvent.getMDCPropertyMap().entrySet()); + for (Map.Entry entry : linkingMetadata.entrySet()) { + combinedContextMap.put(NEW_RELIC_PREFIX + entry.getKey(), entry.getValue()); + } + if (!isNoOpMDC) { + for (Map.Entry entry : linkingMetadata.entrySet()) { + MDC.put(NEW_RELIC_PREFIX + entry.getKey(), entry.getValue()); + } + wrappedEvent = eventObject; + } else { + wrappedEvent = new CustomLoggingEventWrapper(eventObject, combinedContextMap); + } super.append(wrappedEvent); } From 3b5619614eef53852409d7d1d1afeecf74583ee0 Mon Sep 17 00:00:00 2001 From: edeleon Date: Thu, 5 Jun 2025 03:44:38 -0700 Subject: [PATCH 08/41] clean up Event Wrapper --- .../logback13/CustomLoggingEventWrapper.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java b/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java index 4e98c3f..5456233 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java @@ -1,3 +1,8 @@ +/* + * Copyright 2025. New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + package com.newrelic.logging.logback13; import ch.qos.logback.classic.Level; @@ -24,6 +29,11 @@ public Map getMDCPropertyMap() { return customMdc; } + @Override + public Map getMdc() { + return customMdc; + } + @Override public String getThreadName() { return delegate.getThreadName(); @@ -79,11 +89,6 @@ public List getMarkerList() { return delegate.getMarkerList(); } - @Override - public Map getMdc() { - return delegate.getMdc(); - } - @Override public long getTimeStamp() { return delegate.getTimeStamp(); From e568eb060be979c29ac1d8c2e3d903f7926d46ad Mon Sep 17 00:00:00 2001 From: edeleon Date: Thu, 5 Jun 2025 05:50:20 -0700 Subject: [PATCH 09/41] update classes --- .../logging/logback13/NewRelicEncoder.java | 14 ++++++++++++++ .../logging/logback13/NewRelicJsonLayout.java | 12 ++++++++++-- .../logback13 => test/java}/CustomArgument.java | 0 .../java}/JsonFactoryProvider.java | 0 4 files changed, 24 insertions(+), 2 deletions(-) rename logback13/src/{main/java/com/newrelic/logging/logback13 => test/java}/CustomArgument.java (100%) rename logback13/src/{main/java/com/newrelic/logging/logback13 => test/java}/JsonFactoryProvider.java (100%) diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java index 3f3cbbc..050632f 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java @@ -9,6 +9,8 @@ import ch.qos.logback.core.encoder.Encoder; import ch.qos.logback.core.encoder.EncoderBase; +import java.io.IOException; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; /** @@ -28,10 +30,22 @@ */ public class NewRelicEncoder extends EncoderBase { private final NewRelicJsonLayout layout = new NewRelicJsonLayout(); +// private OutputStream outputStream; + +// public void setOutputStream(OutputStream os) { +// this.outputStream = os; +// } @Override public byte[] encode(ILoggingEvent event) { String laidOutResult = layout.doLayout(event); + byte[] resultArray = laidOutResult.getBytes(StandardCharsets.UTF_8); +// try { +// outputStream.write(resultArray); +// outputStream.flush(); +// } catch (IOException e) { +// throw new RuntimeException(e); +// } return laidOutResult.getBytes(StandardCharsets.UTF_8); } diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java index 433ea15..eb6c404 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java @@ -47,7 +47,6 @@ public String doLayout(ILoggingEvent eventObject) { } sw.append('\n'); - System.out.println("[NewRelicJsonLayout.doLayout] sw.toString() : " + sw.toString()); return sw.toString(); } @@ -75,7 +74,16 @@ private void writeToGenerator(ILoggingEvent eventObject, JsonGenerator generator } } else if (!isNoOpMDC) { for (Map.Entry entry : MDC.getCopyOfContextMap().entrySet()) { - generator.writeStringField(entry.getKey(), entry.getValue()); + String key = entry.getKey(); + String value = entry.getValue(); + if (value == null || value.isEmpty()) { + continue; + } + if (key.startsWith(NEW_RELIC_PREFIX)) { + generator.writeStringField(key.substring(NEW_RELIC_PREFIX.length()), value); + } else { + generator.writeStringField(CONTEXT_PREFIX + key, value); + } } } diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/CustomArgument.java b/logback13/src/test/java/CustomArgument.java similarity index 100% rename from logback13/src/main/java/com/newrelic/logging/logback13/CustomArgument.java rename to logback13/src/test/java/CustomArgument.java diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/JsonFactoryProvider.java b/logback13/src/test/java/JsonFactoryProvider.java similarity index 100% rename from logback13/src/main/java/com/newrelic/logging/logback13/JsonFactoryProvider.java rename to logback13/src/test/java/JsonFactoryProvider.java From 675d5accb0d152e22e7d245d9cb49c239d56fe20 Mon Sep 17 00:00:00 2001 From: edeleon Date: Thu, 5 Jun 2025 05:51:35 -0700 Subject: [PATCH 10/41] moved some files and did some more cleanup --- logback13/src/test/java/CustomArgument.java | 2 - .../src/test/java/JsonFactoryProvider.java | 2 - .../test/java/JsonFactoryProviderTest.java | 1 - .../src/test/java/NewRelicLogback13Tests.java | 307 ++---------------- 4 files changed, 29 insertions(+), 283 deletions(-) diff --git a/logback13/src/test/java/CustomArgument.java b/logback13/src/test/java/CustomArgument.java index e34c556..a8ae6c0 100644 --- a/logback13/src/test/java/CustomArgument.java +++ b/logback13/src/test/java/CustomArgument.java @@ -1,5 +1,3 @@ -package com.newrelic.logging.logback13; - public class CustomArgument { private final String key; private final String value; diff --git a/logback13/src/test/java/JsonFactoryProvider.java b/logback13/src/test/java/JsonFactoryProvider.java index 3dbfba4..d3190d6 100644 --- a/logback13/src/test/java/JsonFactoryProvider.java +++ b/logback13/src/test/java/JsonFactoryProvider.java @@ -1,5 +1,3 @@ -package com.newrelic.logging.logback13; - import com.fasterxml.jackson.core.JsonFactory; public class JsonFactoryProvider { diff --git a/logback13/src/test/java/JsonFactoryProviderTest.java b/logback13/src/test/java/JsonFactoryProviderTest.java index b7b1d04..27ea8b7 100644 --- a/logback13/src/test/java/JsonFactoryProviderTest.java +++ b/logback13/src/test/java/JsonFactoryProviderTest.java @@ -1,5 +1,4 @@ import com.fasterxml.jackson.core.JsonFactory; -import com.newrelic.logging.logback13.JsonFactoryProvider; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/logback13/src/test/java/NewRelicLogback13Tests.java b/logback13/src/test/java/NewRelicLogback13Tests.java index afa082a..02c039a 100644 --- a/logback13/src/test/java/NewRelicLogback13Tests.java +++ b/logback13/src/test/java/NewRelicLogback13Tests.java @@ -1,320 +1,71 @@ -/* - * Copyright 2025. New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import ch.qos.logback.classic.AsyncAppender; -import ch.qos.logback.classic.Level; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.LoggingEvent; -import ch.qos.logback.classic.spi.ThrowableProxy; import ch.qos.logback.core.ConsoleAppender; -import com.google.common.collect.ImmutableMap; +import ch.qos.logback.core.read.ListAppender; import com.newrelic.api.agent.Agent; -import com.newrelic.logging.core.LogAsserts; -import com.newrelic.logging.logback13.CustomArgument; import com.newrelic.logging.logback13.NewRelicAsyncAppender; import com.newrelic.logging.logback13.NewRelicEncoder; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; import org.mockito.Mockito; -import org.slf4j.LoggerFactory; import org.slf4j.MDC; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; -import java.nio.charset.StandardCharsets; -import java.util.function.Supplier; -import java.util.regex.Pattern; +import java.util.Map; -import static org.junit.jupiter.api.Assertions.*; +public class NewRelicLogback13Tests { -class NewRelicLogback13Tests { - private AsyncAppender appender; - private LoggingEvent event; - private PipedOutputStream outputStream; - private BufferedReader bufferedReader; - private String output; + private static final LoggerContext loggerContext = new LoggerContext(); + private static final String TEST_MESSAGE = "This is an amazing test message."; - private static Supplier savedSupplier; - private boolean isNoOpMDC; + private NewRelicAsyncAppender appender; + private ListAppender listAppender; @BeforeEach - void setUp() throws Exception { - // Clear MDC data before each test - isNoOpMDC = NewRelicAsyncAppender.isNoOpMDC; - MDC.clear(); - outputStream = new PipedOutputStream(); - PipedInputStream inputStream = new PipedInputStream(outputStream); - bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); - } - - @AfterEach - void tearDown() throws Exception { - // Clear MDC data before each test - MDC.clear(); - NewRelicAsyncAppender.isNoOpMDC = isNoOpMDC; - appender.stop(); - appender.detachAndStopAllAppenders(); - outputStream.close(); - bufferedReader.close(); - } - - @BeforeAll - static void setUpClass() { - savedSupplier = NewRelicAsyncAppender.agentSupplier; - } - - @AfterAll - static void tearDownClass() { - NewRelicAsyncAppender.agentSupplier = savedSupplier; - } - - @Test - @Timeout(3) - void shouldWrapJsonConsoleAppenderCorrectly() throws Throwable { - givenMockAgentData(); - givenARedirectedAppender(); - givenALoggingEvent(); - whenTheEventIsAppended(); - thenMockAgentDataIsInTheMessage(); - thenJsonLayoutWasUsed(); - } - - @Test - @Timeout(3) - void shouldAllWorkCorrectlyEvenWithoutMDC() throws Throwable { - givenMockAgentData(); - givenARedirectedAppender(); - givenMDCIsANoOp(); - givenALoggingEvent(); - whenTheEventIsAppended(); - thenMockAgentDataIsInTheMessage(); - thenJsonLayoutWasUsed(); - } - - @Test - @Timeout(3) - void shouldAppendCallerDataToJsonCorrectly() throws Throwable { - givenMockAgentData(); - givenARedirectedAppender(); - givenALoggingEventWithCallerData(); - whenTheEventIsAppended(); - thenJsonLayoutWasUsed(); - thenTheCallerDataIsInTheMessage(); - } - - @Test - @Timeout(3) - void shouldAppendErrorDataCorrectly() throws Throwable { - givenMockAgentData(); - givenARedirectedAppender(); - givenALoggingEventWithExceptionData(); - whenTheEventIsAppended(); - thenJsonLayoutWasUsed(); - thenTheExceptionDataIsInTheMessage(); - } - - @Test - @Timeout(3) - void shouldAppendCustomArgsToJsonCorrectly() throws Throwable { - givenMockAgentData(); - givenARedirectedAppender(); - givenALoggingEventWithCustomArgs(); - whenTheEventIsAppended(); - thenJsonLayoutWasUsed(); - thenTheCustomArgsAreInTheMessage(); - } - - @Test - @Timeout(3) - void shouldAppendMDCArgsToJsonWhenEnabled() throws Throwable { - givenMockAgentData(); - givenARedirectedAppender(); - givenALoggingEventWithMDCEnabled(); - whenTheEventIsAppended(); - thenJsonLayoutWasUsed(); - thenTheMDCFieldsAreInTheMessage(true); - } - - @Test - @Timeout(3) - void shouldNotAppendMDCArgsToJsonWhenDisabled() throws Throwable { - givenMockAgentData(); - givenARedirectedAppender(); - givenALoggingEventWithMDCDisabled(); - whenTheEventIsAppended(); - thenJsonLayoutWasUsed(); - thenTheMDCFieldsAreInTheMessage(false); - } - - private void givenMockAgentData() { + void setup() { Agent mockAgent = Mockito.mock(Agent.class); - Mockito.when(mockAgent.getLinkingMetadata()).thenReturn(ImmutableMap.of("some.key", "some.value")); + Mockito.when(mockAgent.getLinkingMetadata()).thenReturn(Map.of("traceId", "abd123", "spanId", "xyz789")); NewRelicAsyncAppender.agentSupplier = () -> mockAgent; - } - - private void givenMDCIsANoOp() { - // Wipe the MDC to mimic a NOPMDCAdapter. - NewRelicAsyncAppender.isNoOpMDC = true; - } - - private void givenALoggingEvent() { - event = new LoggingEvent(); - event.setMessage("test_error_message"); - event.setLevel(Level.ERROR); - event.setLoggerContext((LoggerContext) LoggerFactory.getILoggerFactory()); - } - - private void givenALoggingEventWithExceptionData() { - givenALoggingEvent(); - event.setThrowableProxy(new ThrowableProxy(new Exception("some interesting info"))); - } - - private void givenALoggingEventWithCallerData() { - givenALoggingEvent(); - event.setCallerData(new StackTraceElement[] { new Exception().getStackTrace()[0] }); - } - private void givenALoggingEventWithCustomArgs() { - givenALoggingEvent(); - CustomArgument customArgument1 = new CustomArgument("customKey1", "customValue1"); - CustomArgument customArgument2 = new CustomArgument("customKey2", "customValue2"); - Object[] customArgs = new Object[2]; - customArgs[0] = customArgument1; - customArgs[1] = customArgument2; - event.setArgumentArray(customArgs); - } - - private void givenALoggingEventWithMDCEnabled() { - // Enable MDC collection - System.setProperty("newrelic.log_extension.add_mdc", "true"); - - // Add MDC data - MDC.put("contextKey1", "contextData1"); - MDC.put("contextKey2", "contextData2"); - MDC.put("contextKey3", "contextData3"); - - givenALoggingEvent(); - } - - private void givenALoggingEventWithMDCDisabled() { - // Disable MDC collection - System.setProperty("newrelic.log_extension.add_mdc", "false"); - - // Add MDC data - MDC.put("contextKey1", "contextData1"); - MDC.put("contextKey2", "contextData2"); - MDC.put("contextKey3", "contextData3"); - - givenALoggingEvent(); - } - - private void givenARedirectedAppender() { NewRelicEncoder encoder = new NewRelicEncoder(); encoder.start(); - LoggerContext context = new LoggerContext(); ConsoleAppender consoleAppender = new ConsoleAppender<>(); - consoleAppender.setContext(context); + consoleAppender.setContext(loggerContext); consoleAppender.setEncoder(encoder); consoleAppender.start(); - // must be set _after_ start() - consoleAppender.setOutputStream(outputStream); appender = new NewRelicAsyncAppender(); - appender.setContext(context); + appender.setContext(loggerContext); appender.addAppender(consoleAppender); appender.start(); } - private void whenTheEventIsAppended() throws IOException { - appender.doAppend(event); - outputStream.flush(); - } - - private void thenJsonLayoutWasUsed() throws IOException { - LogAsserts.assertFieldValues( - getOutput(), - ImmutableMap.of( - "message", "test_error_message", - "log.level", "ERROR", - "some.key", "some.value" - ) - ); - } - - private void thenMockAgentDataIsInTheMessage() throws Throwable { - assertTrue( - getOutput().contains("some.key=some.value") - || getOutput().contains("\"some.key\":\"some.value\""), - "Expected >>" + getOutput() + "<< to contain some.key to some.value" - ); + @AfterEach + void tearDown() { + MDC.clear(); + appender.stop(); + appender.detachAndStopAllAppenders(); } - private void thenTheCallerDataIsInTheMessage() throws Throwable { - LogAsserts.assertFieldValues( - getOutput(), - ImmutableMap.of("class.name", this.getClass().getName(), "method.name", "givenALoggingEventWithCallerData") - ); - } + @Test + void testBasicLogMessageIncludesLinkingMetadata() { +// LoggingEvent event = createBasicEvent(TEST_MESSAGE); +// appender.doAppend(event); - private void thenTheExceptionDataIsInTheMessage() throws Throwable { - LogAsserts.assertFieldValues( - getOutput(), - ImmutableMap.of( - "error.class", "java.lang.Exception", - "error.stack", Pattern.compile(".*NewRelicLogback13Tests\\.shouldAppendErrorDataCorrectly.*", Pattern.DOTALL), - "error.message", "some error message") - ); - } +// AssertTrue(event.getMDCPropertyMap().containsKey("NewRelic:")); - private void thenTheCustomArgsAreInTheMessage() throws Throwable { - LogAsserts.assertFieldValues( - getOutput(), - ImmutableMap.of("customKey1", "customValue1", "customKey2", "customValue2") - ); - } - private void thenTheMDCFieldsAreInTheMessage(boolean shouldExist) throws Throwable { - String result = getOutput(); - boolean contextKey1Exists = LogAsserts.assertFieldExistence( - "context.contextKey1", - result, - shouldExist - ); - assertEquals(shouldExist, contextKey1Exists, "MDC context.contextKey1 should exist: " + shouldExist); - boolean contextKey2Exists = LogAsserts.assertFieldExistence( - "context.contextKey2", - result, - shouldExist - ); - assertEquals(shouldExist, contextKey2Exists, "MDC context.contextKey2 should exist: " + shouldExist); + ILoggingEvent event = Mockito.mock(ILoggingEvent.class); + Mockito.when(event.getMessage()).thenReturn(TEST_MESSAGE); + Mockito.when(event.getMDCPropertyMap()).thenReturn(Map.of("customKey", "customValue")); - boolean contextKey3Exists = LogAsserts.assertFieldExistence( - "context.contextKey3", - result, - shouldExist - ); - assertEquals(shouldExist, contextKey3Exists, "MDC context.contextKey3 should exist: " + shouldExist); - } + appender.doAppend(event); - private String getOutput() throws IOException { - if (output == null) { - output = bufferedReader.readLine() + "\n"; - appender.stop(); - } - assertNotNull(output); - return output; + // Verify that the message was logged with the expected metadata +// assert appender.getAppender("console").getEncoder().encode(event).contains("traceId\":\"abd123"); +// assert appender.getAppender("console").getEncoder().encode(event).contains("spanId\":\"xyz789"); } - -} +} \ No newline at end of file From 61ab0c069b9c1cc4fe62b0ae6baade80e7b0d660 Mon Sep 17 00:00:00 2001 From: edeleon Date: Thu, 5 Jun 2025 05:52:50 -0700 Subject: [PATCH 11/41] developing new testing suite --- logback13/src/test/java/NewRelicLogback13Tests.java | 1 - 1 file changed, 1 deletion(-) diff --git a/logback13/src/test/java/NewRelicLogback13Tests.java b/logback13/src/test/java/NewRelicLogback13Tests.java index 02c039a..97cc195 100644 --- a/logback13/src/test/java/NewRelicLogback13Tests.java +++ b/logback13/src/test/java/NewRelicLogback13Tests.java @@ -1,6 +1,5 @@ import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.classic.spi.LoggingEvent; import ch.qos.logback.core.ConsoleAppender; import ch.qos.logback.core.read.ListAppender; import com.newrelic.api.agent.Agent; From e97df91889555f88ba9d712547fb648e2d7be227 Mon Sep 17 00:00:00 2001 From: edeleon Date: Thu, 5 Jun 2025 05:55:45 -0700 Subject: [PATCH 12/41] clean up commented out sections --- logback13/spotbugs-filter.xml | 41 ----------------------------------- 1 file changed, 41 deletions(-) diff --git a/logback13/spotbugs-filter.xml b/logback13/spotbugs-filter.xml index da8ce29..f99b510 100644 --- a/logback13/spotbugs-filter.xml +++ b/logback13/spotbugs-filter.xml @@ -7,53 +7,12 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubusercontent.com/spotbugs/spotbugs/3.1.0/spotbugs/etc/findbugsfilter.xsd"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 452884d9ce80b6914aace08b5773d833730828ba Mon Sep 17 00:00:00 2001 From: edeleon Date: Thu, 5 Jun 2025 10:24:48 -0700 Subject: [PATCH 13/41] included a comment to explain the necessity of the wrapper class --- .../logging/logback13/CustomLoggingEventWrapper.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java b/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java index 5456233..3c9447b 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java @@ -15,6 +15,14 @@ import java.util.List; import java.util.Map; +/** + * This wrapper ensures compatibility with Logback 1.3.x, which introduced changes to the ILoggingEvent. ILoggingEvent.getMDCPropertyMap() now returns + * an immutable MDC map and ILoggingEvent.setMDCPropertyMap() is no longer available. + *

+ * This class implements the {@link ILoggingEvent} interface and wraps an existing ILoggingEvent, + * allowing for custom MDC (Mapped Diagnostic Context) properties to be set. + */ + public class CustomLoggingEventWrapper implements ILoggingEvent { private final ILoggingEvent delegate; private final Map customMdc; From 24368f189c571e48262ab29c86afe5a750da1993 Mon Sep 17 00:00:00 2001 From: edeleon Date: Thu, 5 Jun 2025 10:25:21 -0700 Subject: [PATCH 14/41] updated the targetCompatibility and the sourceCompatibility --- logback13/build.gradle.kts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/logback13/build.gradle.kts b/logback13/build.gradle.kts index 4d69045..e129aa4 100644 --- a/logback13/build.gradle.kts +++ b/logback13/build.gradle.kts @@ -1,6 +1,6 @@ plugins { id("java") - id("com.github.spotbugs").version("4.4.4") + id("com.github.spotbugs").version("4.8.0") } group = "com.newrelic.logging" @@ -21,14 +21,14 @@ includeInJar.exclude(group = "org.apache.commons") configurations["compileOnly"].extendsFrom(includeInJar) dependencies { - implementation("com.fasterxml.jackson.core:jackson-core:2.11.1") + implementation("com.fasterxml.jackson.core:jackson-core:2.15.0") implementation("ch.qos.logback:logback-core:1.3.15") implementation("ch.qos.logback:logback-classic:1.3.15") implementation("com.newrelic.agent.java:newrelic-api:7.6.0") includeInJar(project(":core")) testImplementation("org.junit.jupiter:junit-jupiter:5.6.2") - testImplementation("com.google.guava:guava:30.0-jre") + testImplementation("com.google.guava:guava:32.0.1-android") testImplementation("org.mockito:mockito-core:3.4.4") testImplementation(project(":core-test")) } @@ -48,8 +48,8 @@ tasks.withType { } configure { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } tasks.register("sourcesJar") { From b54df83efd398cf37d1ed11f81b8e37a5415f29c Mon Sep 17 00:00:00 2001 From: edeleon Date: Mon, 9 Jun 2025 09:27:47 -0700 Subject: [PATCH 15/41] update field name --- .../newrelic/logging/logback13/CustomLoggingEventWrapper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java b/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java index 3c9447b..3516c30 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java @@ -27,9 +27,9 @@ public class CustomLoggingEventWrapper implements ILoggingEvent { private final ILoggingEvent delegate; private final Map customMdc; - public CustomLoggingEventWrapper(ILoggingEvent delegate, Map mdcOverride) { + public CustomLoggingEventWrapper(ILoggingEvent delegate, Map customContext) { this.delegate = delegate; - this.customMdc = mdcOverride; + this.customMdc = customContext; } @Override From 95bd250a57983a362975fb5c6310efd034b26ef1 Mon Sep 17 00:00:00 2001 From: edeleon Date: Mon, 9 Jun 2025 10:23:12 -0700 Subject: [PATCH 16/41] update class comment --- .../logback13/NewRelicAsyncAppender.java | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java index 128c6d7..063eac5 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java @@ -20,18 +20,18 @@ /** * An {@link AsyncAppender} implementation that synchronously captures New Relic trace data. *

- * This appender will wrap the existing {@link AsyncAppender} logic in order to capture New Relic data - * on the same thread as the log message was created. To use, wrap your existing appender in your - * config xml, and use the async appender in the appropriate logger. + * This appender will wrap the existing {@link AsyncAppender} logic in order to capture New Relic data on + * the same thread as the log message was created. To use, wrap your existing appender in your config xml, + * and use the async appender in the appropriate logger. * *

{@code
- *     
- *         
- *     
+ *      
+ *          
+ *      
  *
- *     
- *         
- *     
+ *      
+ *          
+ *      
  * }
* * @see Logback AsyncAppender @@ -46,7 +46,6 @@ protected void preprocess(ILoggingEvent eventObject) { @Override protected void append(ILoggingEvent eventObject) { - ILoggingEvent wrappedEvent; if (!isStarted()) { return; } @@ -71,11 +70,11 @@ protected void append(ILoggingEvent eventObject) { for (Map.Entry entry : linkingMetadata.entrySet()) { MDC.put(NEW_RELIC_PREFIX + entry.getKey(), entry.getValue()); } - wrappedEvent = eventObject; + super.append(eventObject); } else { - wrappedEvent = new CustomLoggingEventWrapper(eventObject, combinedContextMap); + ILoggingEvent wrappedEvent = new CustomLoggingEventWrapper(eventObject, combinedContextMap); + super.append(wrappedEvent); } - super.append(wrappedEvent); } //visible for testing From 1a8769016e1db2bf9757e033437496b6b3f40891 Mon Sep 17 00:00:00 2001 From: edeleon Date: Mon, 9 Jun 2025 10:25:15 -0700 Subject: [PATCH 17/41] cleaned up class comment and imports --- .../logging/logback13/NewRelicEncoder.java | 34 ++++--------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java index 050632f..aa4b550 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java @@ -6,46 +6,24 @@ package com.newrelic.logging.logback13; import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.encoder.Encoder; import ch.qos.logback.core.encoder.EncoderBase; -import java.io.IOException; -import java.io.OutputStream; import java.nio.charset.StandardCharsets; /** - * An {@link ch.qos.logback.core.encoder.Encoder} that will write New Relic's JSON format. - * - * To use, set this as an encoder on an appender using {@link ch.qos.logback.core.OutputStreamAppender#setEncoder(Encoder)}. - * (This is the base class for both {@link ch.qos.logback.core.rolling.RollingFileAppender} and {@link ch.qos.logback.core.ConsoleAppender}.) - * - *
{@code
- *     
- *         logs/app-log-file.log
- *         
- *     
- * }
- * - * @see Logback Encoders + * An {@link EncoderBase} implementation that serializes {@link ILoggingEvent} instances into JSON format. + *

+ * This encoder is designed to work with logback 1.3.x and uses the {@link NewRelicJsonLayout} to format the log events. + *

+ * Example usage in a logback configuration file: + * */ public class NewRelicEncoder extends EncoderBase { private final NewRelicJsonLayout layout = new NewRelicJsonLayout(); -// private OutputStream outputStream; - -// public void setOutputStream(OutputStream os) { -// this.outputStream = os; -// } @Override public byte[] encode(ILoggingEvent event) { String laidOutResult = layout.doLayout(event); - byte[] resultArray = laidOutResult.getBytes(StandardCharsets.UTF_8); -// try { -// outputStream.write(resultArray); -// outputStream.flush(); -// } catch (IOException e) { -// throw new RuntimeException(e); -// } return laidOutResult.getBytes(StandardCharsets.UTF_8); } From 50a3b50c68dbbf0f8d0d78fe420f53076e5b2c91 Mon Sep 17 00:00:00 2001 From: edeleon Date: Mon, 9 Jun 2025 10:29:31 -0700 Subject: [PATCH 18/41] update class comment --- .../com/newrelic/logging/logback13/NewRelicJsonLayout.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java index eb6c404..abbabaa 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java @@ -31,6 +31,11 @@ import static com.newrelic.logging.core.LogExtensionConfig.CONTEXT_PREFIX; import static com.newrelic.logging.logback13.NewRelicAsyncAppender.NEW_RELIC_PREFIX; +/** + * A layout that formats log events as JSON objects, suitable for use with New Relic. + * Adds standard fields such and injects linking metadata using prefixed MDC keys. + */ + public class NewRelicJsonLayout extends LayoutBase { private boolean started = false; private Context context; @@ -46,7 +51,6 @@ public String doLayout(ILoggingEvent eventObject) { return eventObject.getFormattedMessage(); } - sw.append('\n'); return sw.toString(); } From 648f1974af9b2950fd77418711aaeb8e86a3b612 Mon Sep 17 00:00:00 2001 From: edeleon Date: Fri, 13 Jun 2025 09:56:27 -0700 Subject: [PATCH 19/41] deleted unnecessary classes --- logback13/src/test/java/CustomArgument.java | 21 ------------ .../src/test/java/JsonFactoryProvider.java | 13 -------- .../test/java/JsonFactoryProviderTest.java | 32 ------------------- 3 files changed, 66 deletions(-) delete mode 100644 logback13/src/test/java/CustomArgument.java delete mode 100644 logback13/src/test/java/JsonFactoryProvider.java delete mode 100644 logback13/src/test/java/JsonFactoryProviderTest.java diff --git a/logback13/src/test/java/CustomArgument.java b/logback13/src/test/java/CustomArgument.java deleted file mode 100644 index a8ae6c0..0000000 --- a/logback13/src/test/java/CustomArgument.java +++ /dev/null @@ -1,21 +0,0 @@ -public class CustomArgument { - private final String key; - private final String value; - - public CustomArgument(String key, String value) { - this.key = key; - this.value = value; - } - - public static CustomArgument keyValue(String key, String value) { - return new CustomArgument(key, value); - } - - public String getKey() { - return key; - } - - public String getValue() { - return value; - } -} diff --git a/logback13/src/test/java/JsonFactoryProvider.java b/logback13/src/test/java/JsonFactoryProvider.java deleted file mode 100644 index d3190d6..0000000 --- a/logback13/src/test/java/JsonFactoryProvider.java +++ /dev/null @@ -1,13 +0,0 @@ -import com.fasterxml.jackson.core.JsonFactory; - -public class JsonFactoryProvider { - private static final JsonFactory jsonFactory = new JsonFactory(); - - public static JsonFactory getInstance() { - return jsonFactory; - } - - private JsonFactoryProvider() { - - } -} diff --git a/logback13/src/test/java/JsonFactoryProviderTest.java b/logback13/src/test/java/JsonFactoryProviderTest.java deleted file mode 100644 index 27ea8b7..0000000 --- a/logback13/src/test/java/JsonFactoryProviderTest.java +++ /dev/null @@ -1,32 +0,0 @@ -import com.fasterxml.jackson.core.JsonFactory; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertSame; - -class JsonFactoryProviderTest { - - @Test - void shouldAlwaysGetSameInstance() { - JsonFactory instance1 = JsonFactoryProvider.getInstance(); - JsonFactory instance2 = JsonFactoryProvider.getInstance(); - - assertNotNull(instance1, "Instance 1 should not be null"); - assertNotNull(instance2, "Instance 2 should not be null"); - assertSame(instance1, instance2, "Instances should be the same"); - } - - @Test - void shouldReturnNonNullJsonFactoryInstance() { - JsonFactory jsonFactory = JsonFactoryProvider.getInstance(); - assertNotNull(jsonFactory, "JsonFactory instance should not be null"); - } - - @Test - void shouldReturnSameJsonFactoryInstance() { - JsonFactory firstInstance = JsonFactoryProvider.getInstance(); - JsonFactory secondInstance = JsonFactoryProvider.getInstance(); - assertSame(firstInstance, secondInstance, "Both instances should be the same"); - } - -} From 7efe829a347822002f25f0df2f98635982ad9701 Mon Sep 17 00:00:00 2001 From: edeleon Date: Fri, 13 Jun 2025 09:56:52 -0700 Subject: [PATCH 20/41] updated to spotbugs --- logback13/spotbugs-filter.xml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/logback13/spotbugs-filter.xml b/logback13/spotbugs-filter.xml index f99b510..bf9b229 100644 --- a/logback13/spotbugs-filter.xml +++ b/logback13/spotbugs-filter.xml @@ -6,13 +6,21 @@ + + + + - + \ No newline at end of file From 958d2ef683d90e3e88837647d4dd40346fc46879 Mon Sep 17 00:00:00 2001 From: edeleon Date: Fri, 13 Jun 2025 09:57:23 -0700 Subject: [PATCH 21/41] update to JsonLayout --- .../logging/logback13/NewRelicJsonLayout.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java index abbabaa..b236a21 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java @@ -73,7 +73,21 @@ private void writeToGenerator(ILoggingEvent eventObject, JsonGenerator generator Map mdcPropertyMap = eventObject.getMDCPropertyMap(); if (mdcPropertyMap != null) { + String key; + String value; + for (Map.Entry entry : mdcPropertyMap.entrySet()) { + if (entry.getKey() == null) { + continue; + } else { + key = entry.getKey(); + value = entry.getValue(); + if (key.startsWith(NEW_RELIC_PREFIX)) { + generator.writeStringField(key.substring(NEW_RELIC_PREFIX.length()), value); + } else if (!isNoOpMDC) { + generator.writeStringField(CONTEXT_PREFIX + key, value); + } + } generator.writeStringField(entry.getKey(), entry.getValue()); } } else if (!isNoOpMDC) { From 6713ff08c52710f42e4ece30f8902c8f64f39db2 Mon Sep 17 00:00:00 2001 From: edeleon Date: Tue, 17 Jun 2025 02:00:52 -0700 Subject: [PATCH 22/41] update tests --- .../src/test/java/NewRelicLogback13Tests.java | 241 +++++++++++++++--- 1 file changed, 210 insertions(+), 31 deletions(-) diff --git a/logback13/src/test/java/NewRelicLogback13Tests.java b/logback13/src/test/java/NewRelicLogback13Tests.java index 97cc195..9a3cc2a 100644 --- a/logback13/src/test/java/NewRelicLogback13Tests.java +++ b/logback13/src/test/java/NewRelicLogback13Tests.java @@ -1,70 +1,249 @@ +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.ConsoleAppender; -import ch.qos.logback.core.read.ListAppender; -import com.newrelic.api.agent.Agent; +import ch.qos.logback.core.OutputStreamAppender; import com.newrelic.logging.logback13.NewRelicAsyncAppender; import com.newrelic.logging.logback13.NewRelicEncoder; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; +import org.slf4j.LoggerFactory; import org.slf4j.MDC; +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; -import java.util.Map; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; public class NewRelicLogback13Tests { - private static final LoggerContext loggerContext = new LoggerContext(); - private static final String TEST_MESSAGE = "This is an amazing test message."; + private static Logger logger; + private LoggerContext loggerContext; + private ByteArrayOutputStream outputStream; + + NewRelicAsyncAppender appender; + OutputStreamAppender delegateAppender; - private NewRelicAsyncAppender appender; - private ListAppender listAppender; + private static final String CONTEXT_PREFIX = "context."; @BeforeEach void setup() { - Agent mockAgent = Mockito.mock(Agent.class); - Mockito.when(mockAgent.getLinkingMetadata()).thenReturn(Map.of("traceId", "abd123", "spanId", "xyz789")); - NewRelicAsyncAppender.agentSupplier = () -> mockAgent; + loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + loggerContext.reset(); + + logger = loggerContext.getLogger("TestLogger"); + logger.setLevel(Level.DEBUG); + + outputStream = new ByteArrayOutputStream(); NewRelicEncoder encoder = new NewRelicEncoder(); + encoder.setContext(loggerContext); encoder.start(); - ConsoleAppender consoleAppender = new ConsoleAppender<>(); - consoleAppender.setContext(loggerContext); - consoleAppender.setEncoder(encoder); - consoleAppender.start(); + delegateAppender = new OutputStreamAppender<>(); + delegateAppender.setContext(loggerContext); + delegateAppender.setName("NR_TEST_DELEGATE_APPENDER"); + delegateAppender.setOutputStream(outputStream); + delegateAppender.setEncoder(encoder); + delegateAppender.setImmediateFlush(true); + delegateAppender.start(); appender = new NewRelicAsyncAppender(); appender.setContext(loggerContext); - appender.addAppender(consoleAppender); + appender.setName("NR_TEST_APPENDER"); + appender.addAppender(delegateAppender); appender.start(); + + logger.addAppender(appender); } @AfterEach - void tearDown() { + void teardown() { MDC.clear(); - appender.stop(); - appender.detachAndStopAllAppenders(); } @Test - void testBasicLogMessageIncludesLinkingMetadata() { -// LoggingEvent event = createBasicEvent(TEST_MESSAGE); -// appender.doAppend(event); + void shouldWrapJsonConsoleAppenderCorrectly() throws InterruptedException, IOException { + logger.info("Very interesting test message"); + Thread.sleep(100); + String output = getLogOutput(); -// AssertTrue(event.getMDCPropertyMap().containsKey("NewRelic:")); + assertTrue(logger.isInfoEnabled()); + assertTrue(output.contains("\"message\":\"Very interesting test message\"")); + } + @Test + void shouldAllWorkCorrectlyWithoutMDC() throws InterruptedException { + logger.info("Very interesting test message, no MDC"); + Thread.sleep(100); + String output = getLogOutput(); + assertTrue(output.contains("Very interesting test message, no MDC")); + assertFalse(output.contains(CONTEXT_PREFIX)); + } - ILoggingEvent event = Mockito.mock(ILoggingEvent.class); - Mockito.when(event.getMessage()).thenReturn(TEST_MESSAGE); - Mockito.when(event.getMDCPropertyMap()).thenReturn(Map.of("customKey", "customValue")); + @Test + void shouldAppendCallerDataToJsonCorrectly() throws InterruptedException { + appender.setIncludeCallerData(true); + logger.info("Test message with Caller Data"); - appender.doAppend(event); + Thread.sleep(100); + String output = getLogOutput(); - // Verify that the message was logged with the expected metadata -// assert appender.getAppender("console").getEncoder().encode(event).contains("traceId\":\"abd123"); -// assert appender.getAppender("console").getEncoder().encode(event).contains("spanId\":\"xyz789"); + assertTrue(output.contains("class.name")); + assertTrue(output.contains("method.name")); + assertTrue(output.contains("line.number")); + assertTrue(output.contains("Test message with Caller Data")); } + + @Test + void shouldAppendMDCArgsToJsonWhenEnabled() throws InterruptedException { + MDC.put("userId", "user-123"); + MDC.put("sessionId", "session-456"); + + logger.info("Logging with MDC enabled"); + Thread.sleep(100); + String output = getLogOutput(); + + assertTrue(output.contains("\"context.userId\":\"user-123\"")); + assertTrue(output.contains("\"context.sessionId\":\"session-456\"")); + assertTrue(output.contains("Logging with MDC enabled")); + } + + @Test + void shouldNotAppendMDCArgsToJsonWhenMDCIsDisabled() throws InterruptedException { + NewRelicAsyncAppender.isNoOpMDC = true; + MDC.put("userId", "user-123"); + MDC.clear(); + + logger.info("Logging with MDC disabled"); + Thread.sleep(100); + String output = getLogOutput(); + + assertTrue(output.contains("Logging with MDC disabled")); + assertFalse(output.contains("\"context.userId\":\"user-123\"")); + assertFalse(output.contains("NewRelic:")); + } + + @Test + void shouldSerializeExceptionStackTraceCorrectly() throws InterruptedException { + Exception exception = new IllegalArgumentException("Test exception"); + + logger.error("Logging a test exception", exception); + Thread.sleep(100); + String output = getLogOutput(); + + assertTrue(output.contains("Logging a test exception")); + assertTrue(output.contains("java.lang.IllegalArgumentException")); + assertTrue(output.contains("Test exception")); + assertTrue(output.contains("at ")); + } + + @Test + void shouldIncludeNewRelicLinkingMetadata() throws InterruptedException { + MDC.put("traceId", "abc123"); + MDC.put("spanId", "xyz789"); + MDC.put("entityId", "entity-456"); + + logger.info("Log with New Relic linking metadata"); + Thread.sleep(100); + String output = getLogOutput(); + + assertTrue(output.contains("\"traceId\":\"abc123\"")); + assertTrue(output.contains("\"spanId\":\"xyz789\"")); + assertTrue(output.contains("\"entityId\":\"entity-456\"")); + assertTrue(output.contains("Log with New Relic linking metadata")); + } + + @Test + void shouldIncludeMarkersInJsonOutput() throws InterruptedException { + Marker marker = MarkerFactory.getMarker("TEST_MARKER"); + logger.info(marker, "Log message with marker"); + + Thread.sleep(100); + String output = getLogOutput(); + + assertTrue(output.contains("\"marker\":[\"TEST_MARKER\"]")); + assertTrue(output.contains("Log message with marker")); + } + + @Test + void shouldLogToMultipleAppendersCorrectly() throws InterruptedException { + ByteArrayOutputStream stream1 = new ByteArrayOutputStream(); + OutputStreamAppender appender1 = new OutputStreamAppender<>(); + appender1.setContext(loggerContext); + appender1.setName("NR_TEST_APPENDER_1"); + appender1.setOutputStream(stream1); + appender1.setEncoder(new NewRelicEncoder()); + appender1.start(); + + ByteArrayOutputStream stream2 = new ByteArrayOutputStream(); + OutputStreamAppender appender2 = new OutputStreamAppender<>(); + appender2.setContext(loggerContext); + appender2.setName("NR_TEST_APPENDER_2"); + appender2.setOutputStream(stream2); + appender2.setEncoder(new NewRelicEncoder()); + appender2.start(); + + logger.addAppender(appender1); + logger.addAppender(appender2); + logger.info("Test message for multiple appenders"); + Thread.sleep(100); + + String output1 = stream1.toString(); + String output2 = stream2.toString(); + + assertTrue(output1.contains("\"message\":\"Test message for multiple appenders\"")); + assertTrue(output2.contains("\"message\":\"Test message for multiple appenders\"")); + + assertNotEquals("", output1); + assertNotEquals("", output2); + } + + @Test + void shouldHandleNullOrEmptyMessagesGracefully() throws InterruptedException { + logger.info(null); + Thread.sleep(100); + String output = getLogOutput(); + assertTrue(output.contains("\"message\":null")); + assertTrue(output.contains("\"log.level\":\"INFO\"")); + + logger.info(""); + Thread.sleep(100); + output = getLogOutput(); + assertTrue(output.contains("\"message\":\"\"")); + assertTrue(output.contains("\"log.level\":\"INFO\"")); + } + + @Test + void shouldLogDifferentLevelsCorrectly() throws InterruptedException { + logger.debug("Debug message"); + logger.info("Info message"); + logger.warn("Warn message"); + logger.error("Error message"); + + Thread.sleep(100); + String output = getLogOutput(); + + assertTrue(output.contains("\"message\":\"Info message\"")); + assertTrue(output.contains("\"message\":\"Warn message\"")); + assertTrue(output.contains("\"message\":\"Error message\"")); + assertTrue(output.contains("\"message\":\"Debug message\"")); + + assertTrue(output.contains("\"log.level\":\"DEBUG\"")); + assertTrue(output.contains("\"log.level\":\"INFO\"")); + assertTrue(output.contains("\"log.level\":\"WARN\"")); + assertTrue(output.contains("\"log.level\":\"ERROR\"")); + } + + private String getLogOutput() { + return new String(outputStream.toByteArray(), StandardCharsets.UTF_8); + } + } \ No newline at end of file From e010f3627e813cb84793dbf2b6d2cd28c2dbe0fe Mon Sep 17 00:00:00 2001 From: edeleon Date: Tue, 17 Jun 2025 02:01:50 -0700 Subject: [PATCH 23/41] add including caller data in preprocessing --- .../com/newrelic/logging/logback13/NewRelicAsyncAppender.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java index 063eac5..0e5369c 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java @@ -42,6 +42,9 @@ public class NewRelicAsyncAppender extends AsyncAppender { @Override protected void preprocess(ILoggingEvent eventObject) { eventObject.prepareForDeferredProcessing(); + if (isIncludeCallerData()) { + eventObject.getCallerData(); + } } @Override From ddddbd866ba0357a08c377ac5100bd23ad6ec116 Mon Sep 17 00:00:00 2001 From: edeleon Date: Tue, 17 Jun 2025 02:04:20 -0700 Subject: [PATCH 24/41] add logic for excpetion handling and closing the generator when finished writing --- .../logging/logback13/NewRelicJsonLayout.java | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java index b236a21..faf855c 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java @@ -6,6 +6,8 @@ package com.newrelic.logging.logback13; import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.IThrowableProxy; +import ch.qos.logback.classic.spi.StackTraceElementProxy; import ch.qos.logback.core.Context; import ch.qos.logback.core.LayoutBase; import ch.qos.logback.core.status.ErrorStatus; @@ -14,17 +16,13 @@ import ch.qos.logback.core.status.WarnStatus; import com.fasterxml.jackson.core.JsonGenerator; import com.newrelic.logging.core.ElementName; -import com.newrelic.logging.core.LogExtensionConfig; import com.fasterxml.jackson.core.JsonFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.slf4j.MDC; import org.slf4j.Marker; import org.slf4j.helpers.NOPMDCAdapter; import java.io.IOException; import java.io.StringWriter; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -47,10 +45,10 @@ public String doLayout(ILoggingEvent eventObject) { try { JsonGenerator generator = new JsonFactory().createGenerator(sw); writeToGenerator(eventObject, generator); + generator.close(); } catch (IOException ignored) { return eventObject.getFormattedMessage(); } - return sw.toString(); } @@ -90,7 +88,9 @@ private void writeToGenerator(ILoggingEvent eventObject, JsonGenerator generator } generator.writeStringField(entry.getKey(), entry.getValue()); } - } else if (!isNoOpMDC) { + } + + if (mdcPropertyMap == null && !isNoOpMDC) { for (Map.Entry entry : MDC.getCopyOfContextMap().entrySet()) { String key = entry.getKey(); String value = entry.getValue(); @@ -113,6 +113,22 @@ private void writeToGenerator(ILoggingEvent eventObject, JsonGenerator generator } generator.writeEndArray(); } + + IThrowableProxy throwableProxy = eventObject.getThrowableProxy(); + if (throwableProxy != null) { + generator.writeFieldName("exception"); + generator.writeStartObject(); + generator.writeStringField("type", throwableProxy.getClassName()); + generator.writeStringField("message", throwableProxy.getMessage()); + + generator.writeArrayFieldStart("stackTrace"); + for (StackTraceElementProxy element : throwableProxy.getStackTraceElementProxyArray()) { + generator.writeString(element.toString()); + } + generator.writeEndArray(); + + generator.writeEndObject(); + } generator.writeEndObject(); } From 5e8d3b6134f1d6062e009c249aec56085c1bff04 Mon Sep 17 00:00:00 2001 From: edeleon Date: Wed, 18 Jun 2025 14:43:08 -0700 Subject: [PATCH 25/41] add copyright header --- .../src/test/java/NewRelicLogback13Tests.java | 24 +++++-------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/logback13/src/test/java/NewRelicLogback13Tests.java b/logback13/src/test/java/NewRelicLogback13Tests.java index 9a3cc2a..8ab4b13 100644 --- a/logback13/src/test/java/NewRelicLogback13Tests.java +++ b/logback13/src/test/java/NewRelicLogback13Tests.java @@ -1,3 +1,8 @@ +/* + * Copyright 2025. New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; @@ -15,7 +20,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -145,22 +149,6 @@ void shouldSerializeExceptionStackTraceCorrectly() throws InterruptedException { assertTrue(output.contains("at ")); } - @Test - void shouldIncludeNewRelicLinkingMetadata() throws InterruptedException { - MDC.put("traceId", "abc123"); - MDC.put("spanId", "xyz789"); - MDC.put("entityId", "entity-456"); - - logger.info("Log with New Relic linking metadata"); - Thread.sleep(100); - String output = getLogOutput(); - - assertTrue(output.contains("\"traceId\":\"abc123\"")); - assertTrue(output.contains("\"spanId\":\"xyz789\"")); - assertTrue(output.contains("\"entityId\":\"entity-456\"")); - assertTrue(output.contains("Log with New Relic linking metadata")); - } - @Test void shouldIncludeMarkersInJsonOutput() throws InterruptedException { Marker marker = MarkerFactory.getMarker("TEST_MARKER"); @@ -243,7 +231,7 @@ void shouldLogDifferentLevelsCorrectly() throws InterruptedException { } private String getLogOutput() { - return new String(outputStream.toByteArray(), StandardCharsets.UTF_8); + return outputStream.toString().trim(); } } \ No newline at end of file From 4506ae1d47152ae157b832071200f46186d69533 Mon Sep 17 00:00:00 2001 From: edeleon Date: Wed, 18 Jun 2025 15:45:07 -0700 Subject: [PATCH 26/41] cleaned up code and class comment --- .../logback13/NewRelicAsyncAppender.java | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java index 0e5369c..7650c29 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java @@ -20,9 +20,13 @@ /** * An {@link AsyncAppender} implementation that synchronously captures New Relic trace data. *

- * This appender will wrap the existing {@link AsyncAppender} logic in order to capture New Relic data on - * the same thread as the log message was created. To use, wrap your existing appender in your config xml, - * and use the async appender in the appropriate logger. + * This appender will wrap the existing {@link AsyncAppender} logic in order to capture New Relic linking + * metadata on the same thread as the log message was created. + *

+ * + *

+ * To use, configure this appender in the 'logback.xml' by wrapping the existing appender: + *

* *
{@code
  *      
@@ -68,16 +72,8 @@ protected void append(ILoggingEvent eventObject) {
         for (Map.Entry entry : linkingMetadata.entrySet()) {
             combinedContextMap.put(NEW_RELIC_PREFIX + entry.getKey(), entry.getValue());
         }
-
-        if (!isNoOpMDC) {
-            for (Map.Entry entry : linkingMetadata.entrySet()) {
-                MDC.put(NEW_RELIC_PREFIX + entry.getKey(), entry.getValue());
-            }
-            super.append(eventObject);
-        } else {
-            ILoggingEvent wrappedEvent = new CustomLoggingEventWrapper(eventObject, combinedContextMap);
-            super.append(wrappedEvent);
-        }
+        ILoggingEvent wrappedEvent = new CustomLoggingEventWrapper(eventObject, combinedContextMap);
+        super.append(wrappedEvent);
     }
 
     //visible for testing

From 3d493e0f87083b4335faa8c00ee8ee0ac892ad1f Mon Sep 17 00:00:00 2001
From: edeleon 
Date: Wed, 18 Jun 2025 15:47:29 -0700
Subject: [PATCH 27/41] cleaned up comment

---
 .../com/newrelic/logging/logback13/NewRelicEncoder.java     | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java
index aa4b550..d231dd1 100644
--- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java
+++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicEncoder.java
@@ -12,9 +12,11 @@
 
 /**
  * An {@link EncoderBase} implementation that serializes {@link ILoggingEvent} instances into JSON format.
+ *
  * 

- * This encoder is designed to work with logback 1.3.x and uses the {@link NewRelicJsonLayout} to format the log events. - *

+ * This encoder is designed to work with logback 1.3.x and uses the {@link NewRelicJsonLayout} to format log events. + *

+ * * Example usage in a logback configuration file: * */ From 28c073c46d863d32c1b542004eb3bd0ab156b33a Mon Sep 17 00:00:00 2001 From: edeleon Date: Wed, 18 Jun 2025 15:52:36 -0700 Subject: [PATCH 28/41] update class comment --- .../logging/logback13/CustomLoggingEventWrapper.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java b/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java index 3516c30..9c3c04e 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java @@ -16,11 +16,11 @@ import java.util.Map; /** - * This wrapper ensures compatibility with Logback 1.3.x, which introduced changes to the ILoggingEvent. ILoggingEvent.getMDCPropertyMap() now returns - * an immutable MDC map and ILoggingEvent.setMDCPropertyMap() is no longer available. + * This wrapper ensures compatibility with Logback 1.3.x, which introduced changes to the {@link ILoggingEvent}. + * {@link ILoggingEvent#getMDCPropertyMap()} now returns an immutable MDC map and {@code ILoggingEvent#setMDCPropertyMap()} is no longer available. *

- * This class implements the {@link ILoggingEvent} interface and wraps an existing ILoggingEvent, - * allowing for custom MDC (Mapped Diagnostic Context) properties to be set. + * This class implements the {@link ILoggingEvent} interface and wraps an existing {@code ILoggingEvent}, + * allowing for custom MDC (Mapped Diagnostic Context) map to be injected and returned when queried. */ public class CustomLoggingEventWrapper implements ILoggingEvent { From fd3470372b8607f693ac5e0b1ba3e5149f590b74 Mon Sep 17 00:00:00 2001 From: edeleon Date: Wed, 18 Jun 2025 15:57:17 -0700 Subject: [PATCH 29/41] update class comment --- .../logging/logback13/NewRelicJsonLayout.java | 40 +++++-------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java index faf855c..91488f6 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java @@ -26,12 +26,11 @@ import java.util.List; import java.util.Map; -import static com.newrelic.logging.core.LogExtensionConfig.CONTEXT_PREFIX; -import static com.newrelic.logging.logback13.NewRelicAsyncAppender.NEW_RELIC_PREFIX; - /** - * A layout that formats log events as JSON objects, suitable for use with New Relic. - * Adds standard fields such and injects linking metadata using prefixed MDC keys. + * A custom layout that formats {@link ILoggingEvent} log events as JSON objects. + * Adds standard log fields and enriches logs with linking metadata. + * + * This layout also adds MDC (Mapped Diagnostic Context) values using prefixed keys (e.g., "context.someKey:someValue"). */ public class NewRelicJsonLayout extends LayoutBase { @@ -49,11 +48,11 @@ public String doLayout(ILoggingEvent eventObject) { } catch (IOException ignored) { return eventObject.getFormattedMessage(); } + sw.append("\n"); return sw.toString(); } private void writeToGenerator(ILoggingEvent eventObject, JsonGenerator generator) throws IOException { - boolean isNoOpMDC = MDC.getMDCAdapter() instanceof NOPMDCAdapter; generator.writeStartObject(); generator.writeStringField(ElementName.MESSAGE, eventObject.getFormattedMessage()); @@ -75,33 +74,12 @@ private void writeToGenerator(ILoggingEvent eventObject, JsonGenerator generator String value; for (Map.Entry entry : mdcPropertyMap.entrySet()) { - if (entry.getKey() == null) { + if (entry.getKey() == null || entry.getValue() == null) { continue; - } else { - key = entry.getKey(); - value = entry.getValue(); - if (key.startsWith(NEW_RELIC_PREFIX)) { - generator.writeStringField(key.substring(NEW_RELIC_PREFIX.length()), value); - } else if (!isNoOpMDC) { - generator.writeStringField(CONTEXT_PREFIX + key, value); - } - } - generator.writeStringField(entry.getKey(), entry.getValue()); - } - } - - if (mdcPropertyMap == null && !isNoOpMDC) { - for (Map.Entry entry : MDC.getCopyOfContextMap().entrySet()) { - String key = entry.getKey(); - String value = entry.getValue(); - if (value == null || value.isEmpty()) { - continue; - } - if (key.startsWith(NEW_RELIC_PREFIX)) { - generator.writeStringField(key.substring(NEW_RELIC_PREFIX.length()), value); - } else { - generator.writeStringField(CONTEXT_PREFIX + key, value); } + key = entry.getKey(); + value = entry.getValue(); + generator.writeStringField(key, value); } } From b274ce7b23f36a7a44a2fec57c431008fc7cf630 Mon Sep 17 00:00:00 2001 From: edeleon Date: Wed, 18 Jun 2025 15:58:25 -0700 Subject: [PATCH 30/41] removed extra testing class --- .../src/test/java/NewRelicJsonLayoutTest.java | 39 ------------------- 1 file changed, 39 deletions(-) delete mode 100644 logback13/src/test/java/NewRelicJsonLayoutTest.java diff --git a/logback13/src/test/java/NewRelicJsonLayoutTest.java b/logback13/src/test/java/NewRelicJsonLayoutTest.java deleted file mode 100644 index 8283129..0000000 --- a/logback13/src/test/java/NewRelicJsonLayoutTest.java +++ /dev/null @@ -1,39 +0,0 @@ -import ch.qos.logback.classic.LoggerContext; -import ch.qos.logback.core.status.Status; -import ch.qos.logback.core.status.StatusManager; -import com.newrelic.logging.logback13.NewRelicJsonLayout; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -public class NewRelicJsonLayoutTest { - private NewRelicJsonLayout layout; - private LoggerContext mockContext; - private StatusManager mockStatusManager; - - @BeforeEach - void setUp() { - layout = new NewRelicJsonLayout(); - mockContext = mock(LoggerContext.class); - layout.setContext(mockContext); - mockStatusManager = mock(StatusManager.class); - when(mockContext.getStatusManager()).thenReturn(mockStatusManager); - } - - @Test - void testContextIsSet() { - assertNotNull(layout.getContext(), "Context should not be null"); - assertEquals(mockContext, layout.getContext(), "Context should match the one set in the test"); - } - - @Test - void testLoggingMethodsWithoutNullPointer() { - layout.addInfo("Test Info Message"); - layout.addWarn("Test Warn Message"); - layout.addError("Test Error Message"); - - verify(mockStatusManager, times(3)).add(any(Status.class)); - } -} From 07f2df96b54e64767e078f7c73d5f007e9c89464 Mon Sep 17 00:00:00 2001 From: edeleon Date: Fri, 20 Jun 2025 00:02:53 -0700 Subject: [PATCH 31/41] include README --- logback13/README.md | 62 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 logback13/README.md diff --git a/logback13/README.md b/logback13/README.md new file mode 100644 index 0000000..c38e6a7 --- /dev/null +++ b/logback13/README.md @@ -0,0 +1,62 @@ +# The New Relic Logback 1.3.x Extension + +## Preconditions + +1. logback 1.3.x must be configured and working in the application. +2. The New Relic Java agent must be enabled using the `-javaagent` command-line parameter. +3. You must be using at least version 8.21.0 of the Java Agent. + +## Configuring + +There are some required changes to your application's logging configuration to use the New Relic +Logback Extension for Logback-1.3. All steps are required. + +**Optional**: [Configuration Options](../README.md#configuration-options) for collecting MDC or controlling stack trace behavior. + +--- + +### 1. Include the dependency in your project. + +Refer to [Maven Central](https://search.maven.org/search?q=g:com.newrelic.logging%20a:logback13) for the appropriate snippets. + +--- + +### 2. Configure an `` element with a `NewRelicEncoder`. + +Update your `logback.xml` to include the `` element like below. + +```xml + + logs/app-log-file.log + + +``` + +*Why?* The New Relic log format is a tailored JSON format with specific fields in specific places +that our log forwarder plugins and back end rely on. At this time, we don't support any customization +of that format. + +### 3. `NewRelicAsyncAppender` must wrap any appenders that will target New Relic's log forwarder + +Update your logging configuration xml to add this section. Change `"LOG_FILE"` to the `name` of the appender +you updated in the previous step. + +```xml + + + +``` + +*Why?* The New Relic log format includes New Relic-specific data that must be captured on the thread the log message +is coming from. This appender captures that information before passing to the standard `AsyncAppender` logic. + +### 4. The Async Appender must be referenced by all loggers + +Update your logging configuration xml to connect the root (and other) loggers to the `ASYNC` appender you configured +in the previous step. + +```xml + + + +``` From 41289700694e731fda011b34f014e49b63cb29d3 Mon Sep 17 00:00:00 2001 From: edeleon Date: Fri, 20 Jun 2025 00:05:48 -0700 Subject: [PATCH 32/41] clean up spacing --- logback13/README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/logback13/README.md b/logback13/README.md index c38e6a7..7a67bef 100644 --- a/logback13/README.md +++ b/logback13/README.md @@ -13,14 +13,10 @@ Logback Extension for Logback-1.3. All steps are required. **Optional**: [Configuration Options](../README.md#configuration-options) for collecting MDC or controlling stack trace behavior. ---- - ### 1. Include the dependency in your project. Refer to [Maven Central](https://search.maven.org/search?q=g:com.newrelic.logging%20a:logback13) for the appropriate snippets. ---- - ### 2. Configure an `` element with a `NewRelicEncoder`. Update your `logback.xml` to include the `` element like below. From e900c49f0259c93ec6073d3788058ed974b9a057 Mon Sep 17 00:00:00 2001 From: edeleon Date: Fri, 20 Jun 2025 00:24:19 -0700 Subject: [PATCH 33/41] added logback.xml example --- logback13/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/logback13/README.md b/logback13/README.md index 7a67bef..9340b0a 100644 --- a/logback13/README.md +++ b/logback13/README.md @@ -56,3 +56,20 @@ in the previous step. ``` +### 5. Example `logback.xml` +```xml + + + logs/app-log-file.log + + + + + + + + + + + +``` \ No newline at end of file From 828aa689c089f6fdf59f57a6a37b79af447026ec Mon Sep 17 00:00:00 2001 From: edeleon Date: Fri, 20 Jun 2025 04:22:17 -0700 Subject: [PATCH 34/41] update test suite package location --- .../newrelic/logging/logback13}/NewRelicLogback13Tests.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename logback13/src/test/java/{ => com/newrelic/logging/logback13}/NewRelicLogback13Tests.java (100%) diff --git a/logback13/src/test/java/NewRelicLogback13Tests.java b/logback13/src/test/java/com/newrelic/logging/logback13/NewRelicLogback13Tests.java similarity index 100% rename from logback13/src/test/java/NewRelicLogback13Tests.java rename to logback13/src/test/java/com/newrelic/logging/logback13/NewRelicLogback13Tests.java From bffa2c5c3f6b656ddd5e1fd713c83e6223da2aed Mon Sep 17 00:00:00 2001 From: edeleon Date: Fri, 20 Jun 2025 04:26:30 -0700 Subject: [PATCH 35/41] update the logic to return mutable mdc object --- .../logging/logback13/CustomLoggingEventWrapper.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java b/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java index 9c3c04e..f2ef8a5 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/CustomLoggingEventWrapper.java @@ -12,6 +12,7 @@ import org.slf4j.Marker; import org.slf4j.event.KeyValuePair; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -34,12 +35,12 @@ public CustomLoggingEventWrapper(ILoggingEvent delegate, Map cus @Override public Map getMDCPropertyMap() { - return customMdc; + return new HashMap<>(customMdc); // Returns a mutable copy of the custom MDC map } @Override public Map getMdc() { - return customMdc; + return new HashMap<>(customMdc); // Returns a mutable copy of the custom MDC map } @Override From 1ff18bef3e1705b60ce1dd2c6cef842605630155 Mon Sep 17 00:00:00 2001 From: edeleon Date: Fri, 20 Jun 2025 04:29:07 -0700 Subject: [PATCH 36/41] clean imports and update package declaration --- .../newrelic/logging/logback13/NewRelicLogback13Tests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/logback13/src/test/java/com/newrelic/logging/logback13/NewRelicLogback13Tests.java b/logback13/src/test/java/com/newrelic/logging/logback13/NewRelicLogback13Tests.java index 8ab4b13..c2913d8 100644 --- a/logback13/src/test/java/com/newrelic/logging/logback13/NewRelicLogback13Tests.java +++ b/logback13/src/test/java/com/newrelic/logging/logback13/NewRelicLogback13Tests.java @@ -3,13 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ +package com.newrelic.logging.logback13; + import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.OutputStreamAppender; -import com.newrelic.logging.logback13.NewRelicAsyncAppender; -import com.newrelic.logging.logback13.NewRelicEncoder; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; From b369db1a5060860d7b9f29dca5bce1ae17be5f8f Mon Sep 17 00:00:00 2001 From: edeleon Date: Fri, 20 Jun 2025 04:30:18 -0700 Subject: [PATCH 37/41] update spotbugs to suppress warnings --- logback13/spotbugs-filter.xml | 47 ++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/logback13/spotbugs-filter.xml b/logback13/spotbugs-filter.xml index bf9b229..e0291c7 100644 --- a/logback13/spotbugs-filter.xml +++ b/logback13/spotbugs-filter.xml @@ -15,12 +15,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + \ No newline at end of file From 025a551c36e26d0927f455e10b0aa78d19c04734 Mon Sep 17 00:00:00 2001 From: edeleon Date: Fri, 20 Jun 2025 04:47:48 -0700 Subject: [PATCH 38/41] clean comments --- logback13/spotbugs-filter.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/logback13/spotbugs-filter.xml b/logback13/spotbugs-filter.xml index e0291c7..b96f194 100644 --- a/logback13/spotbugs-filter.xml +++ b/logback13/spotbugs-filter.xml @@ -21,7 +21,6 @@ --> - From 42b9fa577f560d0b01c5d3f1d9f79522264b0f06 Mon Sep 17 00:00:00 2001 From: edeleon Date: Thu, 26 Jun 2025 14:19:27 -0700 Subject: [PATCH 39/41] remove unnecessary null check --- .../com/newrelic/logging/logback13/NewRelicAsyncAppender.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java index 7650c29..f92e2fd 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java @@ -62,9 +62,7 @@ protected void append(ILoggingEvent eventObject) { Map copyMdc = MDC.getMDCAdapter().getCopyOfContextMap(); if (copyMdc != null) { for (Map.Entry entry : copyMdc.entrySet()) { - if (entry.getKey() != null && entry.getValue() != null) { - combinedContextMap.put(CONTEXT_PREFIX + entry.getKey(), entry.getValue()); - } + combinedContextMap.put(CONTEXT_PREFIX + entry.getKey(), entry.getValue()); } } From 4916d9ab645fe74bfb7082a3f3a4e72b3e53e9f7 Mon Sep 17 00:00:00 2001 From: edeleon Date: Thu, 26 Jun 2025 14:20:32 -0700 Subject: [PATCH 40/41] remove unnecessary null check --- .../com/newrelic/logging/logback13/NewRelicJsonLayout.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java index 91488f6..a425bd3 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java @@ -74,9 +74,6 @@ private void writeToGenerator(ILoggingEvent eventObject, JsonGenerator generator String value; for (Map.Entry entry : mdcPropertyMap.entrySet()) { - if (entry.getKey() == null || entry.getValue() == null) { - continue; - } key = entry.getKey(); value = entry.getValue(); generator.writeStringField(key, value); From 505eb0765ee7948371ac0ccb64e6f4a3626b8149 Mon Sep 17 00:00:00 2001 From: edeleon Date: Fri, 27 Jun 2025 11:29:45 -0700 Subject: [PATCH 41/41] update null check --- .../com/newrelic/logging/logback13/NewRelicAsyncAppender.java | 4 +++- .../com/newrelic/logging/logback13/NewRelicJsonLayout.java | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java index f92e2fd..41f0d61 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicAsyncAppender.java @@ -62,7 +62,9 @@ protected void append(ILoggingEvent eventObject) { Map copyMdc = MDC.getMDCAdapter().getCopyOfContextMap(); if (copyMdc != null) { for (Map.Entry entry : copyMdc.entrySet()) { - combinedContextMap.put(CONTEXT_PREFIX + entry.getKey(), entry.getValue()); + if (entry.getValue() != null) { + combinedContextMap.put(CONTEXT_PREFIX + entry.getKey(), entry.getValue()); + } } } diff --git a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java index a425bd3..2530d21 100644 --- a/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java +++ b/logback13/src/main/java/com/newrelic/logging/logback13/NewRelicJsonLayout.java @@ -74,6 +74,9 @@ private void writeToGenerator(ILoggingEvent eventObject, JsonGenerator generator String value; for (Map.Entry entry : mdcPropertyMap.entrySet()) { + if (entry.getValue() == null) { + continue; + } key = entry.getKey(); value = entry.getValue(); generator.writeStringField(key, value);