Skip to content

Commit bbddab8

Browse files
authored
Merge branch 'master' into fix-2321
2 parents 28d8ea3 + 4063989 commit bbddab8

File tree

16 files changed

+384
-25
lines changed

16 files changed

+384
-25
lines changed

CHANGELOG.asciidoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@ endif::[]
2525
2626
[float]
2727
===== Features
28+
* Exceptions that are logged using the fatal log level are now captured (log4j2 only) - {pull}2377[#2377]
2829
* Replaced `authorization` in the default value of `sanitize_field_names` with `*auth*` - {pull}2326[#2326]
2930
3031
[float]
3132
===== Bug fixes
33+
* Fix runtime attach with some docker images - {pull}2385[#2385]
34+
* Restore dynamic capability to `log_level` config for plugin loggers - {pull}2384[#2384]
3235
3336
[[release-notes-1.x]]
3437
=== Java Agent version 1.x

Jenkinsfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,8 @@ pipeline {
335335
axis {
336336
// the list of support java versions can be found in the infra repo (ansible/roles/java/defaults/main.yml)
337337
name 'JAVA_VERSION'
338-
values 'openjdk12', 'openjdk13', 'openjdk14', 'openjdk17'
338+
// 'openjdk18' disabled for now see https://github.com/elastic/apm-agent-java/issues/2328
339+
values 'openjdk17'
339340

340341
}
341342
}

apm-agent-attach/src/main/java/co/elastic/apm/attach/ElasticApmAttacher.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,14 +151,34 @@ public static void attach(String pid, Map<String, String> configuration, File ag
151151
File tempFile = createTempProperties(configuration, null);
152152
String agentArgs = tempFile == null ? null : TEMP_PROPERTIES_FILE_KEY + "=" + tempFile.getAbsolutePath();
153153

154-
ByteBuddyAgent.attach(agentJarFile, pid, agentArgs, ElasticAttachmentProvider.get());
154+
attachWithFallback(agentJarFile, pid, agentArgs);
155155
if (tempFile != null) {
156156
if (!tempFile.delete()) {
157157
tempFile.deleteOnExit();
158158
}
159159
}
160160
}
161161

162+
private static void attachWithFallback(File agentJarFile, String pid, String agentArgs) {
163+
try {
164+
// while the native providers may report to be supported and appear to work properly, in practice there are
165+
// cases (Docker without '--init' option on some JDK images like 'openjdk:8-jdk-alpine') where the accessor
166+
// returned by the provider will not work as expected at attachment time.
167+
ByteBuddyAgent.attach(agentJarFile, pid, agentArgs, ElasticAttachmentProvider.get());
168+
} catch (RuntimeException e1) {
169+
try {
170+
ByteBuddyAgent.attach(agentJarFile, pid, agentArgs, ElasticAttachmentProvider.getFallback());
171+
} catch (RuntimeException e2) {
172+
// output the two exceptions for debugging
173+
System.err.println("Unable to attach with fallback provider:");
174+
e2.printStackTrace();
175+
176+
System.err.println("Unable to attach with regular provider:");
177+
e1.printStackTrace();
178+
}
179+
}
180+
}
181+
162182
/**
163183
* Attaches the agent to a remote JVM
164184
*
@@ -168,7 +188,7 @@ public static void attach(String pid, Map<String, String> configuration, File ag
168188
*/
169189
@Deprecated
170190
public static void attach(String pid, String agentArgs) {
171-
ByteBuddyAgent.attach(AgentJarFileHolder.INSTANCE.agentJarFile, pid, agentArgs, ElasticAttachmentProvider.get());
191+
attachWithFallback(AgentJarFileHolder.INSTANCE.agentJarFile, pid, agentArgs);
172192
}
173193

174194
public static File getBundledAgentJarFile() {

apm-agent-attach/src/main/java/co/elastic/apm/attach/ElasticAttachmentProvider.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ public class ElasticAttachmentProvider {
2727

2828
private static ByteBuddyAgent.AttachmentProvider provider;
2929

30+
private static ByteBuddyAgent.AttachmentProvider fallback;
31+
3032
/**
3133
* Initializes attachment provider, this method can only be called once as it loads native code.
32-
*
3334
*/
3435
private synchronized static void init() {
3536
if (provider != null) {
@@ -44,14 +45,21 @@ private synchronized static void init() {
4445
new CachedAttachmentProvider(ByteBuddyAgent.AttachmentProvider.ForStandardToolsJarVm.MACINTOSH),
4546
new CachedAttachmentProvider(ByteBuddyAgent.AttachmentProvider.ForUserDefinedToolsJar.INSTANCE),
4647
// only use emulated attach last, as native attachment providers should be preferred
47-
ByteBuddyAgent.AttachmentProvider.ForEmulatedAttachment.INSTANCE);
48+
getFallback());
4849

4950

5051
provider = new ByteBuddyAgent.AttachmentProvider.Compound(providers);
5152
}
5253

54+
private synchronized static void initFallback(){
55+
if (fallback != null) {
56+
throw new IllegalStateException("ElasticAttachmentProvider.initFallback() should only be called once");
57+
}
58+
fallback = ByteBuddyAgent.AttachmentProvider.ForEmulatedAttachment.INSTANCE;
59+
}
60+
5361
/**
54-
* Get (and optionally initialize) attachment provider, will internally call {@link #init()} if not already called
62+
* Get (and optionally initialize) attachment provider
5563
*
5664
* @return attachment provider
5765
*/
@@ -62,4 +70,16 @@ public synchronized static ByteBuddyAgent.AttachmentProvider get() {
6270
return provider;
6371
}
6472

73+
/**
74+
* Get (and optionally initialize) fallback (emulated) attachment provider
75+
*
76+
* @return fallback (emulated) attachment provider
77+
*/
78+
public synchronized static ByteBuddyAgent.AttachmentProvider getFallback() {
79+
if (fallback == null) {
80+
initFallback();
81+
}
82+
return fallback;
83+
}
84+
6585
}

apm-agent-core/src/main/java/co/elastic/apm/agent/logging/Log4j2ConfigurationFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public Configuration getConfiguration(LoggerContext loggerContext, Configuration
118118
return getConfiguration();
119119
}
120120

121-
public Configuration getConfiguration() {
121+
Configuration getConfiguration() {
122122
ConfigurationBuilder<BuiltConfiguration> builder = newConfigurationBuilder();
123123
builder.setStatusLevel(Level.ERROR)
124124
.setConfigurationName("ElasticAPM");

apm-agent-core/src/main/java/co/elastic/apm/agent/logging/LoggingConfiguration.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,16 @@
2323
import co.elastic.apm.agent.matcher.WildcardMatcher;
2424
import co.elastic.apm.agent.matcher.WildcardMatcherValueConverter;
2525
import org.apache.logging.log4j.Level;
26+
import org.apache.logging.log4j.LogManager;
27+
import org.apache.logging.log4j.core.LoggerContext;
2628
import org.apache.logging.log4j.core.config.ConfigurationFactory;
2729
import org.apache.logging.log4j.core.config.Configurator;
30+
import org.apache.logging.log4j.core.config.LoggerConfig;
31+
import org.apache.logging.log4j.core.impl.Log4jContextFactory;
32+
import org.apache.logging.log4j.core.selector.ContextSelector;
33+
import org.apache.logging.log4j.spi.LoggerContextFactory;
2834
import org.apache.logging.log4j.status.StatusLogger;
35+
import org.slf4j.LoggerFactory;
2936
import org.stagemonitor.configuration.ConfigurationOption;
3037
import org.stagemonitor.configuration.ConfigurationOptionProvider;
3138
import org.stagemonitor.configuration.converter.ListValueConverter;
@@ -343,7 +350,23 @@ private static void setLogLevel(@Nullable LogLevel level) {
343350
if (level == null) {
344351
level = LogLevel.INFO;
345352
}
346-
Configurator.setRootLevel(org.apache.logging.log4j.Level.toLevel(level.toString(), org.apache.logging.log4j.Level.INFO));
353+
Level log4jLevel = Level.toLevel(level.toString(), Level.INFO);
354+
LoggerContextFactory contextFactory = LogManager.getFactory();
355+
if (contextFactory instanceof Log4jContextFactory) {
356+
final ContextSelector selector = ((Log4jContextFactory) contextFactory).getSelector();
357+
for (LoggerContext loggerContext : selector.getLoggerContexts()) {
358+
// Taken from org.apache.logging.log4j.core.config.Configurator#setRootLevel()
359+
final LoggerConfig loggerConfig = loggerContext.getConfiguration().getRootLogger();
360+
if (!loggerConfig.getLevel().equals(log4jLevel)) {
361+
loggerConfig.setLevel(log4jLevel);
362+
loggerContext.updateLoggers();
363+
}
364+
}
365+
} else {
366+
// it should be safe to obtain a logger here
367+
LoggerFactory.getLogger(LoggingConfiguration.class).warn("Unexpected type of LoggerContextFactory - {}, " +
368+
"cannot update logging level", contextFactory);
369+
}
347370

348371
// Setting the root level resets all the other loggers that may have been configured, which overrides
349372
// configuration provided by the configuration files in the classpath. While the JSON schema validator is only

apm-agent-core/src/test/java/co/elastic/apm/agent/impl/metadata/ContainerInfoTest.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@
2020

2121
import co.elastic.apm.agent.util.CustomEnvVariables;
2222
import org.junit.jupiter.api.Test;
23-
import org.junit.jupiter.api.condition.DisabledOnJre;
24-
import org.junit.jupiter.api.condition.JRE;
2523

2624
import javax.annotation.Nullable;
2725

@@ -131,7 +129,6 @@ void testKubernetesInfo_containerd_cri() {
131129
}
132130

133131
@Test
134-
@DisabledOnJre({JRE.JAVA_15, JRE.JAVA_16}) // https://github.com/elastic/apm-agent-java/issues/1942
135132
void testKubernetesDownwardApi() throws Exception {
136133
String line = "1:name=systemd:/kubepods/besteffort/pode9b90526-f47d-11e8-b2a5-080027b9f4fb/15aa6e53-b09a-40c7-8558-c6c31e36c88a";
137134
String containerId = "15aa6e53-b09a-40c7-8558-c6c31e36c88a";
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
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.logging;
20+
21+
import co.elastic.apm.agent.MockTracer;
22+
import co.elastic.apm.agent.bci.ElasticApmAgent;
23+
import co.elastic.apm.agent.bci.classloading.IndyPluginClassLoader;
24+
import co.elastic.apm.agent.configuration.SpyConfiguration;
25+
import co.elastic.apm.agent.impl.ElasticApmTracer;
26+
import co.elastic.apm.agent.logging.instr.LoggerTestInstrumentation;
27+
import net.bytebuddy.agent.ByteBuddyAgent;
28+
import org.apache.logging.log4j.LogManager;
29+
import org.apache.logging.log4j.core.LoggerContext;
30+
import org.apache.logging.log4j.core.config.Configuration;
31+
import org.apache.logging.log4j.core.config.ConfigurationSource;
32+
import org.apache.logging.log4j.core.impl.Log4jContextFactory;
33+
import org.apache.logging.log4j.core.selector.ContextSelector;
34+
import org.apache.logging.log4j.spi.LoggerContextFactory;
35+
import org.junit.jupiter.api.AfterAll;
36+
import org.junit.jupiter.api.AfterEach;
37+
import org.junit.jupiter.api.BeforeAll;
38+
import org.junit.jupiter.api.Test;
39+
import org.slf4j.Logger;
40+
import org.slf4j.LoggerFactory;
41+
import org.stagemonitor.configuration.ConfigurationOption;
42+
43+
import javax.annotation.Nullable;
44+
import java.io.IOException;
45+
import java.net.URI;
46+
import java.util.HashMap;
47+
import java.util.List;
48+
import java.util.Map;
49+
import java.util.Objects;
50+
51+
import static org.assertj.core.api.Assertions.assertThat;
52+
53+
class LoggingConfigurationTest {
54+
55+
private static LoggerContextFactory originalLoggerContextFactory;
56+
private static Logger agentLogger;
57+
58+
private static ConfigurationOption<LogLevel> logLevelConfig;
59+
private static TestLog4jContextFactory testLog4jContextFactory;
60+
61+
62+
@BeforeAll
63+
static void setup() {
64+
ElasticApmTracer tracer = MockTracer.createRealTracer();
65+
//noinspection unchecked
66+
logLevelConfig = (ConfigurationOption<LogLevel>) tracer.getConfigurationRegistry().getConfigurationOptionByKey("log_level");
67+
ElasticApmAgent.initInstrumentation(tracer, ByteBuddyAgent.install(), List.of(new LoggerTestInstrumentation()));
68+
69+
// We need to clean the current contexts for this test to resemble early agent setup
70+
originalLoggerContextFactory = LogManager.getFactory();
71+
// Setting with a custom context factory that create a new context for each ClassLoader. This is similar to the default
72+
// context log4j2 context factory, with one exception - this simple factory doesn't consider ClassLoader hierarchy
73+
// as the context, but each ClassLoader is consider a different context.
74+
// This better reflects the runtime environment of real agent, where the agent CL is not part of the CL hierarchy
75+
// of plugin class loaders (it's contents are available, but it is not part of the inherent CL hierarchy)
76+
testLog4jContextFactory = new TestLog4jContextFactory();
77+
LogManager.setFactory(testLog4jContextFactory);
78+
79+
// Not really an agent logger, but representing the agent-level logger
80+
agentLogger = LoggerFactory.getLogger(LoggingConfigurationTest.class);
81+
}
82+
83+
@AfterAll
84+
static void reset() {
85+
ElasticApmAgent.reset();
86+
}
87+
88+
@AfterEach
89+
void tearDown() {
90+
// restoring the original logger context factory so that other tests are unaffected
91+
LogManager.setFactory(originalLoggerContextFactory);
92+
}
93+
94+
@Test
95+
void loggingLevelChangeTest() throws IOException {
96+
// Assuming default is debug level in tests based on test.elasticapm.properties
97+
assertThat(agentLogger.isTraceEnabled()).isFalse();
98+
// A logger created by a plugin CL - see LoggerTestInstrumentation
99+
Logger pluginLogger = new LoggerTest().getLogger();
100+
assertThat(pluginLogger).isNotNull();
101+
LoggerContext pluginLoggerContext = testLog4jContextFactory.getContext(pluginLogger);
102+
assertThat(pluginLoggerContext).isNotNull();
103+
assertThat(pluginLoggerContext.getName()).startsWith(IndyPluginClassLoader.class.getName());
104+
assertThat(pluginLogger.isTraceEnabled()).isFalse();
105+
106+
logLevelConfig.update(LogLevel.TRACE, SpyConfiguration.CONFIG_SOURCE_NAME);
107+
assertThat(agentLogger.isTraceEnabled()).isTrue();
108+
assertThat(pluginLogger.isTraceEnabled()).isTrue();
109+
}
110+
111+
private static class LoggerTest {
112+
@Nullable
113+
Logger getLogger() {
114+
return null;
115+
}
116+
}
117+
118+
private static final class TestLog4jContextFactory extends Log4jContextFactory {
119+
120+
ContextSelector contextSelector = new TestContextSelector();
121+
122+
@Nullable
123+
LoggerContext getContext(Logger slf4jLogger) {
124+
for (LoggerContext loggerContext : contextSelector.getLoggerContexts()) {
125+
for (org.apache.logging.log4j.core.Logger log4jLogger : loggerContext.getLoggers()) {
126+
if (log4jLogger.getName().equals(slf4jLogger.getName())) {
127+
return loggerContext;
128+
}
129+
}
130+
}
131+
return null;
132+
}
133+
134+
@Override
135+
public LoggerContext getContext(String fqcn, ClassLoader loader, Object externalContext, boolean currentContext) {
136+
return contextSelector.getContext(fqcn, loader, currentContext);
137+
}
138+
139+
@Override
140+
public LoggerContext getContext(String fqcn, ClassLoader loader, Object externalContext, boolean currentContext, URI configLocation, String name) {
141+
return getContext(fqcn, loader, externalContext, currentContext);
142+
}
143+
144+
@Override
145+
public LoggerContext getContext(String fqcn, ClassLoader loader, Object externalContext, boolean currentContext, ConfigurationSource source) {
146+
return getContext(fqcn, loader, externalContext, currentContext);
147+
}
148+
149+
@Override
150+
public LoggerContext getContext(String fqcn, ClassLoader loader, Object externalContext, boolean currentContext, Configuration configuration) {
151+
return getContext(fqcn, loader, externalContext, currentContext);
152+
}
153+
154+
@Override
155+
public LoggerContext getContext(String fqcn, ClassLoader loader, Object externalContext, boolean currentContext, List<URI> configLocations, String name) {
156+
return getContext(fqcn, loader, externalContext, currentContext);
157+
}
158+
159+
@Override
160+
public ContextSelector getSelector() {
161+
return contextSelector;
162+
}
163+
164+
@Override
165+
public void removeContext(org.apache.logging.log4j.spi.LoggerContext context) {
166+
if (context instanceof LoggerContext) {
167+
contextSelector.removeContext((LoggerContext) context);
168+
}
169+
}
170+
171+
private class TestContextSelector implements ContextSelector {
172+
private final Map<ClassLoader, LoggerContext> contextMap = new HashMap<>();
173+
174+
@Override
175+
public LoggerContext getContext(String fqcn, ClassLoader loader, boolean currentContext) {
176+
if (loader == null) {
177+
loader = ClassLoader.getSystemClassLoader();
178+
}
179+
LoggerContext loggerContext = contextMap.get(loader);
180+
if (loggerContext == null) {
181+
org.apache.logging.log4j.core.LoggerContext loggerContextImpl = new org.apache.logging.log4j.core.LoggerContext(Objects.toString(loader));
182+
// This mimics the actual mechanism - configuration will be applied here
183+
loggerContextImpl.start();
184+
loggerContext = loggerContextImpl;
185+
contextMap.put(loader, loggerContext);
186+
}
187+
return loggerContext;
188+
}
189+
190+
@Override
191+
public LoggerContext getContext(String fqcn, ClassLoader loader, boolean currentContext, URI configLocation) {
192+
return getContext(fqcn, loader, currentContext);
193+
}
194+
195+
@Override
196+
public List<LoggerContext> getLoggerContexts() {
197+
return List.copyOf(contextMap.values());
198+
}
199+
200+
@Override
201+
public void removeContext(LoggerContext context) {
202+
contextMap.values().removeIf(curr -> curr.equals(context));
203+
}
204+
}
205+
}
206+
}

0 commit comments

Comments
 (0)