Skip to content

Commit 06f171f

Browse files
JUL log correlation (#2724)
Co-authored-by: eyalkoren <41850454+eyalkoren@users.noreply.github.com>
1 parent 2dcc82e commit 06f171f

File tree

33 files changed

+482
-92
lines changed

33 files changed

+482
-92
lines changed

CHANGELOG.asciidoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ endif::[]
2828
2929
[float]
3030
===== Features
31-
* Cool new feature: {pull}2526[#2526]
31+
* Add support for log correlation for `java.util.logging` (JUL) - {pull}2724[#2724]
3232
3333
[float]
3434
===== Bug fixes

apm-agent-attach-cli/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
<dependency>
4343
<groupId>co.elastic.logging</groupId>
4444
<artifactId>log4j2-ecs-layout</artifactId>
45-
<version>${version.log4j2-ecs-layout}</version>
45+
<version>${version.ecs.logging}</version>
4646
</dependency>
4747
<dependency>
4848
<groupId>org.bouncycastle</groupId>

apm-agent-core/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
<dependency>
7878
<groupId>co.elastic.logging</groupId>
7979
<artifactId>log4j2-ecs-layout</artifactId>
80-
<version>${version.log4j2-ecs-layout}</version>
80+
<version>${version.ecs.logging}</version>
8181
</dependency>
8282

8383
<dependency>

apm-agent-core/src/main/java/co/elastic/apm/agent/bci/bytebuddy/ClassLoaderNameMatcher.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,6 @@ public boolean matches(ClassLoader target) {
4747
};
4848
}
4949

50-
51-
5250
public static ElementMatcher.Junction<ClassLoader> isReflectionClassLoader() {
5351
return classLoaderWithName("sun.reflect.DelegatingClassLoader")
5452
.or(classLoaderWithName("jdk.internal.reflect.DelegatingClassLoader"));

apm-agent-core/src/main/java/co/elastic/apm/agent/bci/bytebuddy/CustomElementMatchers.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,23 @@
5353
public class CustomElementMatchers {
5454

5555
private static final Logger logger = LoggerFactory.getLogger(CustomElementMatchers.class);
56+
5657
private static final ElementMatcher.Junction.AbstractBase<ClassLoader> AGENT_CLASS_LOADER_MATCHER = new ElementMatcher.Junction.AbstractBase<ClassLoader>() {
5758
@Override
5859
public boolean matches(@Nullable ClassLoader classLoader) {
5960
return ClassLoaderUtils.isAgentClassLoader(classLoader);
6061
}
6162
};
6263

64+
private static final ElementMatcher.Junction.AbstractBase<ClassLoader> INTERNAL_PLUGIN_CLASS_LOADER_MATCHER = new ElementMatcher.Junction.AbstractBase<ClassLoader>() {
65+
@Override
66+
public boolean matches(@Nullable ClassLoader classLoader) {
67+
68+
boolean result = ClassLoaderUtils.isInternalPluginClassLoader(classLoader);
69+
return result;
70+
}
71+
};
72+
6373
public static ElementMatcher.Junction<NamedElement> isInAnyPackage(Collection<String> includedPackages,
6474
ElementMatcher.Junction<NamedElement> defaultIfEmpty) {
6575
if (includedPackages.isEmpty()) {
@@ -74,6 +84,7 @@ public static ElementMatcher.Junction<NamedElement> isInAnyPackage(Collection<St
7484

7585
/**
7686
* Matches the target class loader to a given class loader by instance comparison
87+
*
7788
* @param other the class loader to match to
7889
* @return {@code true} if {@code other} is the same class loader instance as the target class loader
7990
*/
@@ -86,7 +97,6 @@ public boolean matches(@Nullable ClassLoader target) {
8697
};
8798
}
8899

89-
90100
/**
91101
* Matches only class loaders which can load a certain class.
92102
* <p>
@@ -188,6 +198,10 @@ public static ElementMatcher.Junction<ClassLoader> isAgentClassLoader() {
188198
return AGENT_CLASS_LOADER_MATCHER;
189199
}
190200

201+
public static ElementMatcher.Junction<ClassLoader> isInternalPluginClassLoader() {
202+
return INTERNAL_PLUGIN_CLASS_LOADER_MATCHER;
203+
}
204+
191205
private enum Matcher {
192206
LTE {
193207
@Override
@@ -202,6 +216,7 @@ <T extends Comparable<T>> boolean match(T c1, T c2) {
202216

203217
}
204218
};
219+
205220
abstract <T extends Comparable<T>> boolean match(T c1, T c2);
206221
}
207222

apm-agent-core/src/main/java/co/elastic/apm/agent/util/ClassLoaderUtils.java

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,31 +18,12 @@
1818
*/
1919
package co.elastic.apm.agent.util;
2020

21+
import co.elastic.apm.agent.bci.classloading.IndyPluginClassLoader;
22+
2123
import javax.annotation.Nullable;
2224

2325
public class ClassLoaderUtils {
2426

25-
/**
26-
* Checks whether the provided {@link ClassLoader} may be unloaded like a web application class loader, for example.
27-
* <p>
28-
* If the class loader can't be unloaded, it is safe to use {@link ThreadLocal}s and to reuse the {@code WeakConcurrentMap.LookupKey}.
29-
* Otherwise, the use of {@link ThreadLocal}s may lead to class loader leaks as it prevents the class loader this class
30-
* is loaded by to unload.
31-
* </p>
32-
*
33-
* @param classLoader The class loader to check.
34-
* @return {@code true} if the provided class loader can be unloaded.
35-
*/
36-
public static boolean isPersistentClassLoader(@Nullable ClassLoader classLoader) {
37-
try {
38-
return classLoader == null // bootstrap class loader
39-
|| classLoader == ClassLoader.getSystemClassLoader()
40-
|| classLoader == ClassLoader.getSystemClassLoader().getParent(); // ext/platfrom class loader;
41-
} catch (Throwable ignored) {
42-
return false;
43-
}
44-
}
45-
4627
public static boolean isAgentClassLoader(@Nullable ClassLoader classLoader) {
4728
return (classLoader != null && classLoader.getClass().getName().startsWith("co.elastic.apm")) ||
4829
// This one also covers unit tests, where the app class loader loads the agent
@@ -52,4 +33,11 @@ public static boolean isAgentClassLoader(@Nullable ClassLoader classLoader) {
5233
public static boolean isBootstrapClassLoader(@Nullable ClassLoader classLoader) {
5334
return classLoader == null;
5435
}
36+
37+
public static boolean isInternalPluginClassLoader(@Nullable ClassLoader classLoader) {
38+
if (classLoader == null) {
39+
return false;
40+
}
41+
return IndyPluginClassLoader.class.getName().equals(classLoader.getClass().getName());
42+
}
5543
}

apm-agent-plugins/apm-ecs-logging-plugin/pom.xml

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,25 @@
1919
<dependency>
2020
<groupId>co.elastic.logging</groupId>
2121
<artifactId>log4j-ecs-layout</artifactId>
22-
<version>1.4.0</version>
22+
<version>${version.ecs.logging}</version>
2323
<scope>provided</scope>
2424
</dependency>
2525
<dependency>
2626
<groupId>co.elastic.logging</groupId>
2727
<artifactId>log4j2-ecs-layout</artifactId>
28-
<version>1.4.0</version>
28+
<version>${version.ecs.logging}</version>
29+
<scope>provided</scope>
30+
</dependency>
31+
<dependency>
32+
<groupId>co.elastic.logging</groupId>
33+
<artifactId>jul-ecs-formatter</artifactId>
34+
<version>${version.ecs.logging}</version>
35+
<scope>provided</scope>
36+
</dependency>
37+
<dependency>
38+
<groupId>co.elastic.apm</groupId>
39+
<artifactId>apm-logging-plugin-common</artifactId>
40+
<version>${project.version}</version>
2941
<scope>provided</scope>
3042
</dependency>
3143
<dependency>
@@ -39,6 +51,7 @@
3951
<artifactId>ivy</artifactId>
4052
<scope>test</scope>
4153
</dependency>
54+
4255
</dependencies>
4356

4457
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package co.elastic.apm.agent.ecs_logging;
20+
21+
import co.elastic.apm.agent.bci.TracerAwareInstrumentation;
22+
import co.elastic.apm.agent.bci.bytebuddy.CustomElementMatchers;
23+
import co.elastic.apm.agent.loginstr.correlation.CorrelationIdMapAdapter;
24+
import co.elastic.logging.jul.EcsFormatter;
25+
import net.bytebuddy.asm.Advice;
26+
import net.bytebuddy.description.method.MethodDescription;
27+
import net.bytebuddy.description.type.TypeDescription;
28+
import net.bytebuddy.matcher.ElementMatcher;
29+
30+
import java.util.Arrays;
31+
import java.util.Collection;
32+
import java.util.Map;
33+
34+
import static net.bytebuddy.matcher.ElementMatchers.named;
35+
import static net.bytebuddy.matcher.ElementMatchers.not;
36+
37+
/**
38+
* Instruments {@link EcsFormatter#getMdcEntries()} to provide correlation IDs at runtime.
39+
* Application(s) copies of ecs-logging and the one in the agent will be instrumented, hence providing log correlation
40+
* for all of them.
41+
*/
42+
@SuppressWarnings("JavadocReference")
43+
public class JulEcsFormatterInstrumentation extends TracerAwareInstrumentation {
44+
45+
@Override
46+
public ElementMatcher.Junction<ClassLoader> getClassLoaderMatcher() {
47+
// ECS formatter that is loaded within the agent should not be instrumented
48+
return not(CustomElementMatchers.isInternalPluginClassLoader());
49+
}
50+
51+
@Override
52+
public Collection<String> getInstrumentationGroupNames() {
53+
return Arrays.asList("logging", "jul-ecs");
54+
}
55+
56+
@Override
57+
public ElementMatcher<? super TypeDescription> getTypeMatcher() {
58+
return named("co.elastic.logging.jul.EcsFormatter");
59+
}
60+
61+
@Override
62+
public ElementMatcher<? super MethodDescription> getMethodMatcher() {
63+
return named("getMdcEntries");
64+
}
65+
66+
public static class AdviceClass {
67+
68+
@Advice.AssignReturned.ToReturned
69+
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class, inline = false)
70+
public static Map<String, String> onExit() {
71+
return CorrelationIdMapAdapter.get();
72+
}
73+
74+
}
75+
76+
77+
}
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
co.elastic.apm.agent.ecs_logging.Log4j2ServiceNameInstrumentation
22
co.elastic.apm.agent.ecs_logging.Log4j2ServiceVersionInstrumentation
3+
4+
co.elastic.apm.agent.ecs_logging.JulEcsFormatterInstrumentation
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package co.elastic.apm.agent.ecs_logging;
20+
21+
import co.elastic.apm.agent.AbstractInstrumentationTest;
22+
import co.elastic.apm.agent.impl.error.ErrorCapture;
23+
import co.elastic.apm.agent.impl.transaction.Transaction;
24+
import co.elastic.logging.jul.EcsFormatter;
25+
import com.fasterxml.jackson.core.JsonProcessingException;
26+
import com.fasterxml.jackson.databind.JsonNode;
27+
import com.fasterxml.jackson.databind.ObjectMapper;
28+
import org.junit.jupiter.api.Test;
29+
30+
import java.util.logging.Level;
31+
import java.util.logging.LogRecord;
32+
33+
import static co.elastic.apm.agent.testutils.assertions.Assertions.assertThat;
34+
35+
public class JulEcsFormatterInstrumentationTest extends AbstractInstrumentationTest {
36+
37+
@Test
38+
void testNoCorrelation() {
39+
JsonNode logLine = logSomething();
40+
41+
assertThat(logLine.get("transaction.id")).isNull();
42+
assertThat(logLine.get("trace.id")).isNull();
43+
}
44+
45+
@Test
46+
void testActiveTransaction() {
47+
Transaction transaction = startTestRootTransaction("log");
48+
try {
49+
JsonNode logLine = logSomething();
50+
51+
assertThat(logLine.get("transaction.id").textValue()).isEqualTo(transaction.getTraceContext().getTransactionId().toString());
52+
assertThat(logLine.get("trace.id").textValue()).isEqualTo(transaction.getTraceContext().getTraceId().toString());
53+
} finally {
54+
transaction.deactivate().end();
55+
}
56+
}
57+
58+
@Test
59+
void testActiveError() {
60+
ErrorCapture error = new ErrorCapture(tracer);
61+
62+
error.activate();
63+
try {
64+
JsonNode logLine = logSomething();
65+
66+
assertThat(logLine.get("error.id").textValue()).isEqualTo(error.getTraceContext().getId().toString());
67+
} finally {
68+
error.deactivate();
69+
}
70+
71+
}
72+
73+
private static JsonNode logSomething() {
74+
EcsFormatter formatter = new EcsFormatter();
75+
LogRecord record = new LogRecord(Level.INFO, "msg");
76+
try {
77+
return new ObjectMapper().readTree(formatter.format(record));
78+
} catch (JsonProcessingException e) {
79+
throw new RuntimeException(e);
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)