Skip to content

Commit ce271a8

Browse files
authored
Fix dynamic log_level configuration (elastic#2384)
1 parent cb34d60 commit ce271a8

File tree

6 files changed

+295
-3
lines changed

6 files changed

+295
-3
lines changed

CHANGELOG.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ endif::[]
2626
[float]
2727
===== Bug fixes
2828
* Fix runtime attach with some docker images - {pull}2385[#2385]
29+
* Restore dynamic capability to `log_level` config for plugin loggers - {pull}2384[#2384]
2930
3031
[[release-notes-1.x]]
3132
=== Java Agent version 1.x

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
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+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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.instr;
20+
21+
import co.elastic.apm.agent.bci.TracerAwareInstrumentation;
22+
import net.bytebuddy.asm.Advice;
23+
import net.bytebuddy.description.method.MethodDescription;
24+
import net.bytebuddy.description.type.TypeDescription;
25+
import net.bytebuddy.implementation.bytecode.assign.Assigner;
26+
import net.bytebuddy.matcher.ElementMatcher;
27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
29+
30+
import java.util.Collection;
31+
import java.util.List;
32+
33+
import static net.bytebuddy.matcher.ElementMatchers.named;
34+
35+
public class LoggerTestInstrumentation extends TracerAwareInstrumentation {
36+
37+
public static class AdviceClass {
38+
39+
private static final Logger pluginLogger = LoggerFactory.getLogger(AdviceClass.class);
40+
41+
@Advice.AssignReturned.ToReturned(typing = Assigner.Typing.DYNAMIC)
42+
@Advice.OnMethodExit(inline = false)
43+
public static Object onGetLoggerExit() {
44+
return pluginLogger;
45+
}
46+
}
47+
48+
@Override
49+
public ElementMatcher<? super TypeDescription> getTypeMatcher() {
50+
return named("co.elastic.apm.agent.logging.LoggingConfigurationTest$LoggerTest");
51+
}
52+
53+
@Override
54+
public ElementMatcher<? super MethodDescription> getMethodMatcher() {
55+
return named("getLogger");
56+
}
57+
58+
@Override
59+
public Collection<String> getInstrumentationGroupNames() {
60+
return List.of("test");
61+
}
62+
}

apm-agent-plugins/apm-grpc/apm-grpc-test-latest/src/test/java/co/elastic/apm/agent/grpc/latest/testapp/generated/HelloGrpc.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
/**
2424
*/
2525
@javax.annotation.Generated(
26-
value = "by gRPC proto compiler (version 1.43.1)",
26+
value = "by gRPC proto compiler (version 1.43.2)",
2727
comments = "Source: rpc.proto")
2828
@io.grpc.stub.annotations.GrpcGenerated
2929
public final class HelloGrpc {

0 commit comments

Comments
 (0)