Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import co.elastic.otel.dynamicconfig.BlockableMetricExporter;
import co.elastic.otel.dynamicconfig.BlockableSpanExporter;
import co.elastic.otel.dynamicconfig.CentralConfig;
import co.elastic.otel.dynamicconfig.ConfigLogger;
import co.elastic.otel.logging.AgentLog;
import com.google.auto.service.AutoService;
import io.opentelemetry.api.common.AttributeKey;
Expand Down Expand Up @@ -76,6 +77,7 @@ public void customize(AutoConfigurationCustomizer autoConfiguration) {
AgentLog.addSpanLoggingIfRequired(providerBuilder, properties);
return providerBuilder;
});
ConfigLogger.triggerInitialLogConfig();
}

private void configureExporterUserAgentHeaders(AutoConfigurationCustomizer autoConfiguration) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,14 @@ public CompletableResultCode shutdown() {
public void close() {
delegate.close();
}

@Override
public String toString() {
return "BlockableLogRecordExporter{"
+ "sendingLogs="
+ sendingLogs
+ ", delegate="
+ delegate
+ '}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,14 @@ public CompletableResultCode shutdown() {
public void close() {
delegate.close();
}

@Override
public String toString() {
return "BlockableMetricExporter{"
+ "sendingMetrics="
+ sendingMetrics
+ ", delegate="
+ delegate
+ '}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,14 @@ public CompletableResultCode flush() {
public CompletableResultCode shutdown() {
return delegate.shutdown();
}

@Override
public String toString() {
return "BlockableSpanExporter{"
+ "sendingSpans="
+ sendingSpans
+ ", delegate="
+ delegate
+ '}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public static void init(SdkTracerProviderBuilder providerBuilder, ConfigProperti
configuration -> {
logger.fine("Received configuration: " + configuration);
Configs.applyConfigurations(configuration);
ConfigLogger.logConfig();
return CentralConfigurationProcessor.Result.SUCCESS;
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package co.elastic.otel.dynamicconfig;

import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import java.lang.reflect.Field;
import java.util.logging.Logger;

public class ConfigLogger {
private static final int DELAY_IN_SECONDS_AFTER_START_FOR_INITIAL_LOG;
private static final Logger logger = Logger.getLogger(ConfigLogger.class.getName());
private static volatile boolean firstLogged = false;

static {
String envDelay = System.getenv("ELASTIC_OTEL_JAVA_CONFIGURATION_LOG_DELAY_SECONDS");
String propDelay = System.getProperty("elastic.otel.java.configuration.log.delay.seconds");
int intDelay;
if (propDelay != null && propDelay.length() > 0) {
try {
intDelay = Integer.parseInt(propDelay);
Copy link
Contributor

@JonasKunz JonasKunz Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This delay mechanism is a workaround to wait for the opentelemetry SDK to be installed, right?

Have you tried triggering the logging from an AgentExtension instead of from the SDK-autoconfig mechanism?

Based on where it's called to me it looks like the SDK should be initialized properly at that time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't an extension do all sorts including add processors, exporters, etc? If so the sdk instance isn't really "cooked" at that stage - other extensions could still be loaded - so printing it out then will give an incomplete config

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a different thing: The AgentExtensions are executed after the SDK has been autoconfigured (based on the code I linked), and therefore after all SDK AutoConfigurationCustomizerProvider which I assume you are referring to have been executed already.

So AgentExtension can't modify the SDK anymore, they can just customize the ByteBuddy AgentBuilder.

} catch (Exception e) {
intDelay = 30;
}
} else if (envDelay != null && envDelay.length() > 0) {
try {
intDelay = Integer.parseInt(envDelay);
} catch (Exception e) {
intDelay = 30;
}
} else {
intDelay = 30;
}
DELAY_IN_SECONDS_AFTER_START_FOR_INITIAL_LOG = intDelay;
}

public static void logConfig() {
if (firstLogged) {
doLogConfig();
}
}

private static void doLogConfig() {
try {
logger.info("GlobalOpenTelemetry: " + getLogConfigString());
} catch (NoSuchFieldException | IllegalAccessException e) {
firstLogged = false; // resetting so we only log the error once
logger.warning(
"Error getting 'delegate' from " + GlobalOpenTelemetry.get() + ": " + e.getMessage());
}
}

static String getLogConfigString() throws NoSuchFieldException, IllegalAccessException {
OpenTelemetry obfuscatedConfig = GlobalOpenTelemetry.get();
Field config = obfuscatedConfig.getClass().getDeclaredField("delegate");
config.setAccessible(true);
return config.get(obfuscatedConfig).toString();
}

public static void triggerInitialLogConfig() {
new java.util.Timer()
.schedule(
new java.util.TimerTask() {
@Override
public void run() {
firstLogged = true;
doLogConfig();
}
},
1000 * DELAY_IN_SECONDS_AFTER_START_FOR_INITIAL_LOG);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -254,5 +254,10 @@ public TracerConfig apply(InstrumentationScopeInfo scopeInfo) {
public void put(InstrumentationScopeInfo scope, TracerConfig tracerConfig) {
map.put(scope.getName(), tracerConfig);
}

@Override
public String toString() {
return "UpdatableConfigurator{" + "map=" + map + '}';
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package co.elastic.otel.dynamicconfig;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;

public class ConfigLoggerTest {
static final String TARGET_CLASS_NAME = "TempTest321";

@Test
public void checkGlobalOpenTelemetryString() throws IOException {
String output = executeCommand(createTestTargetCommand(), 20);
assertThat(output).contains("ConfigLogger - GlobalOpenTelemetry");
assertThat(output).contains("tracerProvider=SdkTracerProvider");
}

static File createTestTarget() throws IOException {
String targetClass =
"public class "
+ TARGET_CLASS_NAME
+ " {\n"
+ " public static void main(String[] args) {\n"
+ " System.out.println(\"Test started\");\n"
+ " try {Thread.sleep(6_000L);} catch (InterruptedException e) {}\n"
+ " System.out.println(\"Test ended\");\n"
+ " }\n"
+ "}\n";
File targetClassFile =
new File(System.getProperty("java.io.tmpdir"), TARGET_CLASS_NAME + ".java");
targetClassFile.deleteOnExit();
try (BufferedWriter writer = new BufferedWriter(new FileWriter(targetClassFile))) {
writer.write(targetClass);
}
return targetClassFile;
}

static String getAgentJarFile() {
// note File path with / is valid on Windows too
File[] jar =
new File("../agent/build/libs")
.listFiles(
(dir, name) ->
name.matches("elastic-otel-javaagent-\\d\\.\\d\\.\\d-SNAPSHOT\\.jar"));
if (jar == null || jar.length != 1) {
throw new IllegalStateException(
"expecting exactly one agent jar file in ../agent/build/libs");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is technically a cyclic dependency. The test lives in the custom project, the custom project is used as dependency when building the agent jar.

This test in turn adds an implicit test dependency from custom on the agent jar. I haven't tested, but I think this would fail if you run ./gradlew clean check?

Imo it would be cleaner to:

  • Move this test to a project in testing/integration-tests
  • Have a look at this convention on how to declare a dependency on the agent project without adding it to the classpath. You can then pass the path to the agent-jar as a JVM-property to the test, so that we don't have this hardcoded path in the test.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If needed, here is another example in the upstream jmx-metrics tests where the path to the agent jar is provided by gradle through an environment variable: https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/instrumentation/jmx-metrics/library/build.gradle.kts

}
return jar[0].getAbsolutePath();
}

static List<String> createTestTargetCommand() throws IOException {
List<String> command = new ArrayList<>();
command.add("java");
command.add("-Xmx32m");
command.add("-javaagent:" + getAgentJarFile());
command.add("-Delastic.otel.java.configuration.log.delay.seconds=5");
command.add(createTestTarget().getAbsolutePath());
return command;
}

private static void pauseSeconds(int seconds) {
try {
Thread.sleep(seconds * 1_000L);
} catch (InterruptedException e) {
}
}

public static String executeCommand(List<String> command, int timeoutSeconds) throws IOException {
ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(true);
Process childProcess = pb.start();

StringBuilder commandOutput = new StringBuilder();

boolean isAlive = true;
byte[] buffer = new byte[64 * 1000];
InputStream in = childProcess.getInputStream();
// stop trying if the time elapsed exceeds the timeout
while (isAlive && (timeoutSeconds > 0)) {
while (in.available() > 0) {
int lengthRead = in.read(buffer, 0, buffer.length);
commandOutput.append(new String(buffer, 0, lengthRead));
}
pauseSeconds(1);
timeoutSeconds--;
isAlive = childProcess.isAlive();
}
// it can die but still have output available buffered
while (in.available() > 0) {
int lengthRead = in.read(buffer, 0, buffer.length);
commandOutput.append(new String(buffer, 0, lengthRead));
}

// Cleanup as well as I can
boolean exited = false;
try {
exited = childProcess.waitFor(3, TimeUnit.SECONDS);
} catch (InterruptedException e) {
}
if (!exited) {
childProcess.destroy();
pauseSeconds(1);
if (childProcess.isAlive()) {
childProcess.destroyForcibly();
}
}

return commandOutput.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
package co.elastic.otel.config;
package co.elastic.otel.dynamicconfig;

import static org.assertj.core.api.Assertions.assertThat;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,5 +185,10 @@ public CompletableResultCode flush() {
public CompletableResultCode shutdown() {
return enabled.get() ? delegate.shutdown() : CompletableResultCode.ofSuccess();
}

@Override
public String toString() {
return "DebugLogSpanExporter{" + "enabled=" + enabled + ", delegate=" + delegate + '}';
}
}
}
Loading