Skip to content

Commit 38918c8

Browse files
authored
dynamic log level (#613)
1 parent 718f344 commit 38918c8

10 files changed

Lines changed: 299 additions & 0 deletions

File tree

agent/entrypoint/src/main/java/co/elastic/otel/agent/ElasticAgent.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,18 @@
2424
/** Elastic agent entry point, delegates to OpenTelemetry agent */
2525
public class ElasticAgent {
2626

27+
private static final String OTEL_JAVAAGENT_LOGGING = "otel.javaagent.logging";
28+
private static final String OTEL_JAVAAGENT_LOGGING_ENV = "OTEL_JAVAAGENT_LOGGING";
29+
private static final String OTEL_JAVAAGENT_LOGGING_DEFAULT = "simple";
30+
2731
/**
2832
* Entry point for -javaagent JVM argument attach
2933
*
3034
* @param agentArgs agent arguments
3135
* @param inst instrumentation
3236
*/
3337
public static void premain(String agentArgs, Instrumentation inst) {
38+
initLogging();
3439
OpenTelemetryAgent.premain(agentArgs, inst);
3540
}
3641

@@ -41,6 +46,7 @@ public static void premain(String agentArgs, Instrumentation inst) {
4146
* @param inst instrumentation
4247
*/
4348
public static void agentmain(String agentArgs, Instrumentation inst) {
49+
initLogging();
4450
OpenTelemetryAgent.agentmain(agentArgs, inst);
4551
}
4652

@@ -53,5 +59,22 @@ public static void main(String[] args) {
5359
OpenTelemetryAgent.main(args);
5460
}
5561

62+
private static void initLogging() {
63+
64+
// Do not override explicitly provided configuration unless it's using the default as the
65+
// 'simple' provider is not included in this distribution and that triggers an SLF4j error.
66+
if (isLoggingNotDefault(System.getProperty(OTEL_JAVAAGENT_LOGGING))
67+
|| isLoggingNotDefault(System.getenv(OTEL_JAVAAGENT_LOGGING_ENV))) {
68+
return;
69+
}
70+
71+
// must match value returned by ElasticLoggingCustomizer#getName
72+
System.setProperty(OTEL_JAVAAGENT_LOGGING, "elastic");
73+
}
74+
75+
private static boolean isLoggingNotDefault(String value) {
76+
return value != null && !OTEL_JAVAAGENT_LOGGING_DEFAULT.equals(value);
77+
}
78+
5679
private ElasticAgent() {}
5780
}

buildSrc/src/main/kotlin/elastic-otel.agent-packaging-conventions.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,9 @@ tasks {
182182
dependsOn(isolateJavaagentLibs)
183183
configurations = listOf(bootstrapLibs, upstreamAgent)
184184

185+
// exclude slf4j-simple from the shadow jar as we use log4j2-slf4j with internal-logging instead
186+
exclude("inst/io/opentelemetry/javaagent/slf4j/simple/**")
187+
185188
from(isolateJavaagentLibs.get().outputs)
186189

187190
archiveClassifier.set("")

buildscripts/allowed-licenses.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
"moduleLicense": "The 2-Clause BSD License",
99
"moduleVersion": ".*",
1010
"moduleName": "org.hdrhistogram:HdrHistogram"
11+
},
12+
{
13+
"moduleLicense": "MIT License",
14+
"moduleVersion": ".*",
15+
"moduleName": "org.slf4j:slf4j-api"
1116
}
1217
]
1318
}

custom/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ dependencies {
1111
implementation(project(":inferred-spans"))
1212
implementation(project(":universal-profiling-integration"))
1313
implementation(project(":resources"))
14+
implementation(project(":internal-logging", configuration = "shadow"))
1415
instrumentations.forEach {
1516
implementation(project(it))
1617
}

gradle/libs.versions.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ jib = "3.4.5"
44
spotless = "7.0.3"
55
junit = "5.12.2"
66
autoservice = "1.1.1"
7+
log4j2 = "2.24.3"
78

89
# otel protocol (OTLP)
910
opentelemetryProto = "1.3.2-alpha"
@@ -76,12 +77,18 @@ ant = "org.apache.ant:ant:1.10.15"
7677
# ASM is currently only used during compile-time, so it is okay to diverge from the version used in ByteBuddy
7778
asm = "org.ow2.asm:asm:9.8"
7879

80+
slf4j-api = "org.slf4j:slf4j-api:2.0.17"
81+
log4j2-slf4j = { group= "org.apache.logging.log4j", name="log4j-slf4j2-impl", version.ref="log4j2"}
82+
log4j2-core = { group= "org.apache.logging.log4j", name="log4j-core", version.ref="log4j2"}
83+
7984
# Instrumented libraries
8085
openaiClient = "com.openai:openai-java:1.4.1"
8186

8287
[bundles]
8388

8489
semconv = ["opentelemetrySemconv", "opentelemetrySemconvIncubating"]
90+
log4j2 = ["log4j2-core", "log4j2-slf4j"]
91+
8592

8693
[plugins]
8794

internal-logging/build.gradle.kts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
2+
import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer
3+
4+
plugins {
5+
id("elastic-otel.library-packaging-conventions")
6+
id("com.gradleup.shadow")
7+
}
8+
9+
dependencies {
10+
11+
compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-tooling")
12+
compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-bootstrap")
13+
compileOnly(libs.slf4j.api)
14+
implementation(libs.bundles.log4j2)
15+
16+
}
17+
18+
tasks {
19+
val shadowJar by existing(ShadowJar::class) {
20+
// required for META-INF/services files relocation
21+
mergeServiceFiles()
22+
23+
// Excluding property source SPI prevents log4j system properties and env variables that might
24+
// be set at the application level to change the behavior of this internal log4j instance.
25+
exclude("**/*.log4j.util.PropertySource")
26+
27+
transform(Log4j2PluginsCacheFileTransformer::class.java)
28+
29+
// relocate slf4j and log4j for internal logging to prevent any conflict
30+
relocate("org.slf4j", "co.elastic.otel.logging.slf4j")
31+
relocate("org.apache.logging.log4j", "co.elastic.otel.logging.log4j")
32+
relocate("org.apache.logging.slf4j", "co.elastic.otel.logging.log4j.slf4j")
33+
}
34+
35+
assemble {
36+
dependsOn(shadowJar)
37+
}
38+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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.otel.logging;
20+
21+
import org.apache.logging.log4j.Level;
22+
import org.apache.logging.log4j.core.config.Configurator;
23+
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder;
24+
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
25+
import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration;
26+
27+
public class AgentLog {
28+
29+
private static final String PATTERN = "%d{DEFAULT} [%t] %-5level %logger{36} - %msg{nolookups}%n";
30+
31+
// logger is an empty string
32+
private static final String ROOT_LOGGER_NAME = "";
33+
34+
private AgentLog() {}
35+
36+
public static void init() {
37+
38+
ConfigurationBuilder<BuiltConfiguration> conf =
39+
ConfigurationBuilderFactory.newConfigurationBuilder();
40+
41+
conf.add(
42+
conf.newAppender("stdout", "Console")
43+
.add(conf.newLayout("PatternLayout").addAttribute("pattern", PATTERN)));
44+
45+
conf.add(conf.newRootLogger().add(conf.newAppenderRef("stdout")));
46+
47+
Configurator.initialize(conf.build(false));
48+
}
49+
50+
/**
51+
* Sets the agent log level at runtime
52+
*
53+
* @param level log level
54+
*/
55+
public static void setLevel(Level level) {
56+
// Using log4j2 implementation allows to change the log level programmatically at runtime
57+
// which is not directly possible through the slf4j API and simple implementation used in
58+
// upstream distribution.
59+
60+
Configurator.setAllLevels(ROOT_LOGGER_NAME, level);
61+
62+
// when debugging we should avoid very chatty http client debug messages
63+
// this behavior is replicated from the upstream distribution.
64+
if (level.intLevel() >= Level.DEBUG.intLevel()) {
65+
Configurator.setLevel("okhttp3.internal.http2", Level.INFO);
66+
Configurator.setLevel("okhttp3.internal.concurrent.TaskRunner", Level.INFO);
67+
}
68+
}
69+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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.otel.logging;
20+
21+
import com.google.auto.service.AutoService;
22+
import io.opentelemetry.javaagent.bootstrap.InternalLogger;
23+
import io.opentelemetry.javaagent.tooling.LoggingCustomizer;
24+
import io.opentelemetry.javaagent.tooling.config.EarlyInitAgentConfig;
25+
import org.apache.logging.log4j.Level;
26+
import org.slf4j.LoggerFactory;
27+
28+
@AutoService(LoggingCustomizer.class)
29+
public class ElasticLoggingCustomizer implements LoggingCustomizer {
30+
31+
@Override
32+
public String name() {
33+
// must match "otel.javaagent.logging" system property for SPI lookup
34+
return "elastic";
35+
}
36+
37+
@Override
38+
public void init(EarlyInitAgentConfig earlyConfig) {
39+
40+
// trigger loading the slf4j provider from the agent CL, this should load log4j implementation
41+
LoggerFactory.getILoggerFactory();
42+
43+
// make the agent internal logger delegate to slf4j, which will delegate to log4j
44+
InternalLogger.initialize(Slf4jInternalLogger::create);
45+
46+
AgentLog.init();
47+
48+
Level level = null;
49+
if (earlyConfig.getBoolean("otel.javaagent.debug", false)) {
50+
// set debug logging when enabled through configuration to behave like the upstream
51+
// distribution
52+
level = Level.DEBUG;
53+
} else {
54+
String levelConfig = earlyConfig.getString("elastic.otel.javaagent.log.level");
55+
if (levelConfig != null) {
56+
level = Level.getLevel(levelConfig);
57+
}
58+
}
59+
AgentLog.setLevel(level != null ? level : Level.INFO);
60+
}
61+
62+
@Override
63+
public void onStartupSuccess() {}
64+
65+
@SuppressWarnings("CallToPrintStackTrace")
66+
@Override
67+
public void onStartupFailure(Throwable throwable) {
68+
throwable.printStackTrace();
69+
}
70+
}
Lines changed: 82 additions & 0 deletions
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.otel.logging;
20+
21+
import io.opentelemetry.javaagent.bootstrap.InternalLogger;
22+
import javax.annotation.Nullable;
23+
import org.slf4j.Logger;
24+
import org.slf4j.LoggerFactory;
25+
26+
/** Internal logger implementation that delegates to SLF4J */
27+
public class Slf4jInternalLogger implements InternalLogger {
28+
private final Logger logger;
29+
30+
private Slf4jInternalLogger(String name) {
31+
this.logger = LoggerFactory.getLogger(name);
32+
}
33+
34+
public static InternalLogger create(String name) {
35+
return new Slf4jInternalLogger(name);
36+
}
37+
38+
@Override
39+
public boolean isLoggable(Level level) {
40+
switch (level) {
41+
case TRACE:
42+
return logger.isTraceEnabled();
43+
case DEBUG:
44+
return logger.isDebugEnabled();
45+
case INFO:
46+
return logger.isInfoEnabled();
47+
case WARN:
48+
return logger.isWarnEnabled();
49+
case ERROR:
50+
return logger.isErrorEnabled();
51+
default:
52+
throw new IllegalArgumentException("Unsupported level: " + level);
53+
}
54+
}
55+
56+
@Override
57+
public void log(Level level, String s, @Nullable Throwable throwable) {
58+
logger.atLevel(toSlf4jLevel(level)).setCause(throwable).log(s);
59+
}
60+
61+
private static org.slf4j.event.Level toSlf4jLevel(Level level) {
62+
switch (level) {
63+
case TRACE:
64+
return org.slf4j.event.Level.TRACE;
65+
case DEBUG:
66+
return org.slf4j.event.Level.DEBUG;
67+
case INFO:
68+
return org.slf4j.event.Level.INFO;
69+
case WARN:
70+
return org.slf4j.event.Level.WARN;
71+
case ERROR:
72+
return org.slf4j.event.Level.ERROR;
73+
default:
74+
throw new IllegalArgumentException("Unsupported level: " + level);
75+
}
76+
}
77+
78+
@Override
79+
public String name() {
80+
return logger.getName();
81+
}
82+
}

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ include("custom")
1919
include("instrumentation")
2020
include("instrumentation:openai-client-instrumentation:instrumentation-1.1")
2121
include("inferred-spans")
22+
include("internal-logging")
2223
include("resources")
2324
include("runtime-attach")
2425
include("smoke-tests")

0 commit comments

Comments
 (0)