Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
8dfcd59
add logback13 plugin
deleonenriqueta Mar 21, 2025
bcc4a2b
add initial logback13 plugin
deleonenriqueta Mar 21, 2025
b6e7c75
add logback-1.3 plugin
deleonenriqueta May 27, 2025
84b3c48
add linking metadata to the argument array when MDC is disabled
deleonenriqueta May 27, 2025
bcf0c56
update plugin
deleonenriqueta May 29, 2025
5bd43ab
mdc reporting
deleonenriqueta Jun 5, 2025
f6e95b1
refactor and clean up
deleonenriqueta Jun 5, 2025
3b56196
clean up Event Wrapper
deleonenriqueta Jun 5, 2025
e568eb0
update classes
deleonenriqueta Jun 5, 2025
675d5ac
moved some files and did some more cleanup
deleonenriqueta Jun 5, 2025
61ab0c0
developing new testing suite
deleonenriqueta Jun 5, 2025
e97df91
clean up commented out sections
deleonenriqueta Jun 5, 2025
452884d
included a comment to explain the necessity of the wrapper class
deleonenriqueta Jun 5, 2025
24368f1
updated the targetCompatibility and the sourceCompatibility
deleonenriqueta Jun 5, 2025
b54df83
update field name
deleonenriqueta Jun 9, 2025
95bd250
update class comment
deleonenriqueta Jun 9, 2025
1a87690
cleaned up class comment and imports
deleonenriqueta Jun 9, 2025
50a3b50
update class comment
deleonenriqueta Jun 9, 2025
648f197
deleted unnecessary classes
deleonenriqueta Jun 13, 2025
7efe829
updated to spotbugs
deleonenriqueta Jun 13, 2025
958d2ef
update to JsonLayout
deleonenriqueta Jun 13, 2025
6713ff0
update tests
deleonenriqueta Jun 17, 2025
e010f36
add including caller data in preprocessing
deleonenriqueta Jun 17, 2025
ddddbd8
add logic for excpetion handling and closing the generator when finished
deleonenriqueta Jun 17, 2025
5e8d3b6
add copyright header
deleonenriqueta Jun 18, 2025
4506ae1
cleaned up code and class comment
deleonenriqueta Jun 18, 2025
3d493e0
cleaned up comment
deleonenriqueta Jun 18, 2025
28c073c
update class comment
deleonenriqueta Jun 18, 2025
fd34703
update class comment
deleonenriqueta Jun 18, 2025
b274ce7
removed extra testing class
deleonenriqueta Jun 18, 2025
07f2df9
include README
deleonenriqueta Jun 20, 2025
4128970
clean up spacing
deleonenriqueta Jun 20, 2025
e900c49
added logback.xml example
deleonenriqueta Jun 20, 2025
828aa68
update test suite package location
deleonenriqueta Jun 20, 2025
bffa2c5
update the logic to return mutable mdc object
deleonenriqueta Jun 20, 2025
1ff18be
clean imports and update package declaration
deleonenriqueta Jun 20, 2025
b369db1
update spotbugs to suppress warnings
deleonenriqueta Jun 20, 2025
025a551
clean comments
deleonenriqueta Jun 20, 2025
42b9fa5
remove unnecessary null check
deleonenriqueta Jun 26, 2025
4916d9a
remove unnecessary null check
deleonenriqueta Jun 26, 2025
505eb07
update null check
deleonenriqueta Jun 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions logback13/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# 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 `<appender>` element with a `NewRelicEncoder`.

Update your `logback.xml` to include the `<encoder>` element like below.

```xml
<appender name="LOG_FILE" class="ch.qos.logback.core.FileAppender">
<file>logs/app-log-file.log</file>
<encoder class="com.newrelic.logging.logback13.NewRelicEncoder"/>
</appender>
```

*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
<appender name="ASYNC" class="com.newrelic.logging.logback13.NewRelicAsyncAppender">
<appender-ref ref="LOG_FILE" />
</appender>
```

*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
<root level="INFO">
<appender-ref ref="ASYNC" />
</root>
```
### 5. Example `logback.xml`
```xml
<configuration>
<appender name="LOG_FILE" class="ch.qos.logback.core.FileAppender">
<file>logs/app-log-file.log</file>
<encoder class="com.newrelic.logging.logback13.NewRelicEncoder"/>
</appender>

<appender name="ASYNC" class="com.newrelic.logging.logback13.NewRelicAsyncAppender">
<appender-ref ref="LOG_FILE" />
</appender>

<root level="INFO">
<appender-ref ref="ASYNC" />
</root>
</configuration>
```
72 changes: 72 additions & 0 deletions logback13/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
plugins {
id("java")
id("com.github.spotbugs").version("4.8.0")
}

group = "com.newrelic.logging"

// -Prelease=true will render a non-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"

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("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:32.0.1-android")
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<Javadoc> {
enabled = true
(options as? CoreJavadocOptions)?.addStringOption("link", "https://logback.qos.ch/apidocs")
}

configure<JavaPluginConvention> {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

tasks.register<Jar>("sourcesJar") {
from(sourceSets.main.get().allJava)
archiveClassifier.set("sources")
}

tasks.register<Jar>("javadocJar") {
from(tasks.javadoc)
archiveClassifier.set("javadoc")
}

apply(from = "$rootDir/gradle/publish.gradle.kts")

tasks.withType<com.github.spotbugs.snom.SpotBugsTask> {
excludeFilter.set(file("spotbugs-filter.xml"))
reports.create("html") {
isEnabled = true
}
}
70 changes: 70 additions & 0 deletions logback13/spotbugs-filter.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
~ Copyright 2025. New Relic Corporation. All rights reserved.
~ SPDX-License-Identifier: Apache-2.0
-->
<FindBugsFilter xmlns="https://github.com/spotbugs/filter/3.0.0"
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">

<Match>
<!--
This is not final so that it can be reassigned in tests to inject a mock Agent.
-->
<Bug pattern="MS_SHOULD_BE_FINAL"/>
<Class name="com.newrelic.logging.logback13.NewRelicAsyncAppender"/>
</Match>

<Match>
<!--
We have to implement all methods of the LayoutBase<ILoggingEvent>.
-->
<Bug pattern="EI_EXPOSE_REP2" />
<Class name="com.newrelic.logging.logback13.NewRelicJsonLayout"/>
</Match>

<Match>
<!--
We have to implement all methods of the LayoutBase<ILoggingEvent>.
-->
<Bug pattern="EI_EXPOSE_REP" />
<Class name="com.newrelic.logging.logback13.NewRelicJsonLayout"/>
<Method name="getContext" />
</Match>

<Match>
<!--
This method calls getCallerData() and is required to populate the stack trace in the log event,
even if the return value is ignored.
-->
<Bug pattern="RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT" />
<Class name="com.newrelic.logging.logback13.NewRelicAsyncAppender"/>
<Method name="preprocess" />
</Match>

<Match>
<!--
This method calls getCallerData() and is required to populate the stack trace in the log event,
even if the return value is ignored.
-->
<Bug pattern="EI_EXPOSE_REP2" />
<Class name="com.newrelic.logging.logback13.CustomLoggingEventWrapper"/>
</Match>

<Match>
<!--
Using ByteArrayOutputStream is being used to write the JSON output, and run test assertions.
-->
<bug pattern="DM_DFAULT_ENCODING" />
<Class name="com.newrelic.logging.logback13.NewRelicLogback13Tests"/>
</Match>

<Match>
<!--
We have to write to a static because otherwise this value is set based on a classload.
-->
<Bug pattern="ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD"/>
<Class name="com.newrelic.logging.logback13.NewRelicLogback11Tests"/>
</Match>

</FindBugsFilter>
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* 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;
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.HashMap;
import java.util.List;
import java.util.Map;

/**
* 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.
* <p>
* 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 {
private final ILoggingEvent delegate;
private final Map<String, String> customMdc;

public CustomLoggingEventWrapper(ILoggingEvent delegate, Map<String, String> customContext) {
this.delegate = delegate;
this.customMdc = customContext;
}

@Override
public Map<String, String> getMDCPropertyMap() {
return new HashMap<>(customMdc); // Returns a mutable copy of the custom MDC map
}

@Override
public Map<String, String> getMdc() {
return new HashMap<>(customMdc); // Returns a mutable copy of the custom MDC map
}

@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<Marker> getMarkerList() {
return delegate.getMarkerList();
}

@Override
public long getTimeStamp() {
return delegate.getTimeStamp();
}

@Override
public int getNanoseconds() {
return delegate.getNanoseconds();
}

@Override
public long getSequenceNumber() {
return delegate.getSequenceNumber();
}

@Override
public List<KeyValuePair> getKeyValuePairs() {
return delegate.getKeyValuePairs();
}

@Override
public void prepareForDeferredProcessing() {
delegate.prepareForDeferredProcessing();
}
}
Loading
Loading