Skip to content

Commit 126f04a

Browse files
Merge pull request #109 from newrelic/add-logback13-plugin
Add Logback13 Plugin
2 parents 8ed0cef + 505eb07 commit 126f04a

File tree

9 files changed

+923
-0
lines changed

9 files changed

+923
-0
lines changed

logback13/README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# The New Relic Logback 1.3.x Extension
2+
3+
## Preconditions
4+
5+
1. logback 1.3.x must be configured and working in the application.
6+
2. The New Relic Java agent must be enabled using the `-javaagent` command-line parameter.
7+
3. You must be using at least version 8.21.0 of the Java Agent.
8+
9+
## Configuring
10+
11+
There are some required changes to your application's logging configuration to use the New Relic
12+
Logback Extension for Logback-1.3. All steps are required.
13+
14+
**Optional**: [Configuration Options](../README.md#configuration-options) for collecting MDC or controlling stack trace behavior.
15+
16+
### 1. Include the dependency in your project.
17+
18+
Refer to [Maven Central](https://search.maven.org/search?q=g:com.newrelic.logging%20a:logback13) for the appropriate snippets.
19+
20+
### 2. Configure an `<appender>` element with a `NewRelicEncoder`.
21+
22+
Update your `logback.xml` to include the `<encoder>` element like below.
23+
24+
```xml
25+
<appender name="LOG_FILE" class="ch.qos.logback.core.FileAppender">
26+
<file>logs/app-log-file.log</file>
27+
<encoder class="com.newrelic.logging.logback13.NewRelicEncoder"/>
28+
</appender>
29+
```
30+
31+
*Why?* The New Relic log format is a tailored JSON format with specific fields in specific places
32+
that our log forwarder plugins and back end rely on. At this time, we don't support any customization
33+
of that format.
34+
35+
### 3. `NewRelicAsyncAppender` must wrap any appenders that will target New Relic's log forwarder
36+
37+
Update your logging configuration xml to add this section. Change `"LOG_FILE"` to the `name` of the appender
38+
you updated in the previous step.
39+
40+
```xml
41+
<appender name="ASYNC" class="com.newrelic.logging.logback13.NewRelicAsyncAppender">
42+
<appender-ref ref="LOG_FILE" />
43+
</appender>
44+
```
45+
46+
*Why?* The New Relic log format includes New Relic-specific data that must be captured on the thread the log message
47+
is coming from. This appender captures that information before passing to the standard `AsyncAppender` logic.
48+
49+
### 4. The Async Appender must be referenced by all loggers
50+
51+
Update your logging configuration xml to connect the root (and other) loggers to the `ASYNC` appender you configured
52+
in the previous step.
53+
54+
```xml
55+
<root level="INFO">
56+
<appender-ref ref="ASYNC" />
57+
</root>
58+
```
59+
### 5. Example `logback.xml`
60+
```xml
61+
<configuration>
62+
<appender name="LOG_FILE" class="ch.qos.logback.core.FileAppender">
63+
<file>logs/app-log-file.log</file>
64+
<encoder class="com.newrelic.logging.logback13.NewRelicEncoder"/>
65+
</appender>
66+
67+
<appender name="ASYNC" class="com.newrelic.logging.logback13.NewRelicAsyncAppender">
68+
<appender-ref ref="LOG_FILE" />
69+
</appender>
70+
71+
<root level="INFO">
72+
<appender-ref ref="ASYNC" />
73+
</root>
74+
</configuration>
75+
```

logback13/build.gradle.kts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
plugins {
2+
id("java")
3+
id("com.github.spotbugs").version("4.8.0")
4+
}
5+
6+
group = "com.newrelic.logging"
7+
8+
// -Prelease=true will render a non-snapshot version
9+
// All other values (including unset) will render a snapshot version.
10+
val release: String? by project
11+
val releaseVersion: String by project
12+
version = releaseVersion + if ("true" == release) "" else "-SNAPSHOT"
13+
14+
repositories {
15+
mavenCentral()
16+
maven(url = "https://dl.bintray.com/mockito/maven/")
17+
}
18+
19+
val includeInJar: Configuration by configurations.creating
20+
includeInJar.exclude(group = "org.apache.commons")
21+
configurations["compileOnly"].extendsFrom(includeInJar)
22+
23+
dependencies {
24+
implementation("com.fasterxml.jackson.core:jackson-core:2.15.0")
25+
implementation("ch.qos.logback:logback-core:1.3.15")
26+
implementation("ch.qos.logback:logback-classic:1.3.15")
27+
implementation("com.newrelic.agent.java:newrelic-api:7.6.0")
28+
includeInJar(project(":core"))
29+
30+
testImplementation("org.junit.jupiter:junit-jupiter:5.6.2")
31+
testImplementation("com.google.guava:guava:32.0.1-android")
32+
testImplementation("org.mockito:mockito-core:3.4.4")
33+
testImplementation(project(":core-test"))
34+
}
35+
36+
val jar by tasks.getting(Jar::class) {
37+
from(configurations["includeInJar"].flatMap {
38+
when {
39+
it.isDirectory -> listOf(it)
40+
else -> listOf(zipTree(it))
41+
}
42+
})
43+
}
44+
45+
tasks.withType<Javadoc> {
46+
enabled = true
47+
(options as? CoreJavadocOptions)?.addStringOption("link", "https://logback.qos.ch/apidocs")
48+
}
49+
50+
configure<JavaPluginConvention> {
51+
sourceCompatibility = JavaVersion.VERSION_1_8
52+
targetCompatibility = JavaVersion.VERSION_1_8
53+
}
54+
55+
tasks.register<Jar>("sourcesJar") {
56+
from(sourceSets.main.get().allJava)
57+
archiveClassifier.set("sources")
58+
}
59+
60+
tasks.register<Jar>("javadocJar") {
61+
from(tasks.javadoc)
62+
archiveClassifier.set("javadoc")
63+
}
64+
65+
apply(from = "$rootDir/gradle/publish.gradle.kts")
66+
67+
tasks.withType<com.github.spotbugs.snom.SpotBugsTask> {
68+
excludeFilter.set(file("spotbugs-filter.xml"))
69+
reports.create("html") {
70+
isEnabled = true
71+
}
72+
}

logback13/spotbugs-filter.xml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<!--
3+
~ Copyright 2025. New Relic Corporation. All rights reserved.
4+
~ SPDX-License-Identifier: Apache-2.0
5+
-->
6+
<FindBugsFilter xmlns="https://github.com/spotbugs/filter/3.0.0"
7+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
8+
xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubusercontent.com/spotbugs/spotbugs/3.1.0/spotbugs/etc/findbugsfilter.xsd">
9+
10+
<Match>
11+
<!--
12+
This is not final so that it can be reassigned in tests to inject a mock Agent.
13+
-->
14+
<Bug pattern="MS_SHOULD_BE_FINAL"/>
15+
<Class name="com.newrelic.logging.logback13.NewRelicAsyncAppender"/>
16+
</Match>
17+
18+
<Match>
19+
<!--
20+
We have to implement all methods of the LayoutBase<ILoggingEvent>.
21+
-->
22+
<Bug pattern="EI_EXPOSE_REP2" />
23+
<Class name="com.newrelic.logging.logback13.NewRelicJsonLayout"/>
24+
</Match>
25+
26+
<Match>
27+
<!--
28+
We have to implement all methods of the LayoutBase<ILoggingEvent>.
29+
-->
30+
<Bug pattern="EI_EXPOSE_REP" />
31+
<Class name="com.newrelic.logging.logback13.NewRelicJsonLayout"/>
32+
<Method name="getContext" />
33+
</Match>
34+
35+
<Match>
36+
<!--
37+
This method calls getCallerData() and is required to populate the stack trace in the log event,
38+
even if the return value is ignored.
39+
-->
40+
<Bug pattern="RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT" />
41+
<Class name="com.newrelic.logging.logback13.NewRelicAsyncAppender"/>
42+
<Method name="preprocess" />
43+
</Match>
44+
45+
<Match>
46+
<!--
47+
This method calls getCallerData() and is required to populate the stack trace in the log event,
48+
even if the return value is ignored.
49+
-->
50+
<Bug pattern="EI_EXPOSE_REP2" />
51+
<Class name="com.newrelic.logging.logback13.CustomLoggingEventWrapper"/>
52+
</Match>
53+
54+
<Match>
55+
<!--
56+
Using ByteArrayOutputStream is being used to write the JSON output, and run test assertions.
57+
-->
58+
<bug pattern="DM_DFAULT_ENCODING" />
59+
<Class name="com.newrelic.logging.logback13.NewRelicLogback13Tests"/>
60+
</Match>
61+
62+
<Match>
63+
<!--
64+
We have to write to a static because otherwise this value is set based on a classload.
65+
-->
66+
<Bug pattern="ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD"/>
67+
<Class name="com.newrelic.logging.logback13.NewRelicLogback11Tests"/>
68+
</Match>
69+
70+
</FindBugsFilter>
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright 2025. New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package com.newrelic.logging.logback13;
7+
8+
import ch.qos.logback.classic.Level;
9+
import ch.qos.logback.classic.spi.ILoggingEvent;
10+
import ch.qos.logback.classic.spi.IThrowableProxy;
11+
import ch.qos.logback.classic.spi.LoggerContextVO;
12+
import org.slf4j.Marker;
13+
import org.slf4j.event.KeyValuePair;
14+
15+
import java.util.HashMap;
16+
import java.util.List;
17+
import java.util.Map;
18+
19+
/**
20+
* This wrapper ensures compatibility with Logback 1.3.x, which introduced changes to the {@link ILoggingEvent}.
21+
* {@link ILoggingEvent#getMDCPropertyMap()} now returns an immutable MDC map and {@code ILoggingEvent#setMDCPropertyMap()} is no longer available.
22+
* <p>
23+
* This class implements the {@link ILoggingEvent} interface and wraps an existing {@code ILoggingEvent},
24+
* allowing for custom MDC (Mapped Diagnostic Context) map to be injected and returned when queried.
25+
*/
26+
27+
public class CustomLoggingEventWrapper implements ILoggingEvent {
28+
private final ILoggingEvent delegate;
29+
private final Map<String, String> customMdc;
30+
31+
public CustomLoggingEventWrapper(ILoggingEvent delegate, Map<String, String> customContext) {
32+
this.delegate = delegate;
33+
this.customMdc = customContext;
34+
}
35+
36+
@Override
37+
public Map<String, String> getMDCPropertyMap() {
38+
return new HashMap<>(customMdc); // Returns a mutable copy of the custom MDC map
39+
}
40+
41+
@Override
42+
public Map<String, String> getMdc() {
43+
return new HashMap<>(customMdc); // Returns a mutable copy of the custom MDC map
44+
}
45+
46+
@Override
47+
public String getThreadName() {
48+
return delegate.getThreadName();
49+
}
50+
51+
@Override
52+
public Level getLevel() {
53+
return delegate.getLevel();
54+
}
55+
56+
@Override
57+
public String getMessage() {
58+
return delegate.getMessage();
59+
}
60+
61+
@Override
62+
public Object[] getArgumentArray() {
63+
return delegate.getArgumentArray();
64+
}
65+
66+
@Override
67+
public String getFormattedMessage() {
68+
return delegate.getFormattedMessage();
69+
}
70+
71+
@Override
72+
public String getLoggerName() {
73+
return delegate.getLoggerName();
74+
}
75+
76+
@Override
77+
public LoggerContextVO getLoggerContextVO() {
78+
return delegate.getLoggerContextVO();
79+
}
80+
81+
@Override
82+
public IThrowableProxy getThrowableProxy() {
83+
return delegate.getThrowableProxy();
84+
}
85+
86+
@Override
87+
public StackTraceElement[] getCallerData() {
88+
return delegate.getCallerData();
89+
}
90+
91+
@Override
92+
public boolean hasCallerData() {
93+
return delegate.hasCallerData();
94+
}
95+
96+
@Override
97+
public List<Marker> getMarkerList() {
98+
return delegate.getMarkerList();
99+
}
100+
101+
@Override
102+
public long getTimeStamp() {
103+
return delegate.getTimeStamp();
104+
}
105+
106+
@Override
107+
public int getNanoseconds() {
108+
return delegate.getNanoseconds();
109+
}
110+
111+
@Override
112+
public long getSequenceNumber() {
113+
return delegate.getSequenceNumber();
114+
}
115+
116+
@Override
117+
public List<KeyValuePair> getKeyValuePairs() {
118+
return delegate.getKeyValuePairs();
119+
}
120+
121+
@Override
122+
public void prepareForDeferredProcessing() {
123+
delegate.prepareForDeferredProcessing();
124+
}
125+
}

0 commit comments

Comments
 (0)