diff --git a/custom/src/main/java/co/elastic/otel/ElasticAutoConfigurationCustomizerProvider.java b/custom/src/main/java/co/elastic/otel/ElasticAutoConfigurationCustomizerProvider.java index 2d6263e0..847d8460 100644 --- a/custom/src/main/java/co/elastic/otel/ElasticAutoConfigurationCustomizerProvider.java +++ b/custom/src/main/java/co/elastic/otel/ElasticAutoConfigurationCustomizerProvider.java @@ -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; @@ -76,6 +77,7 @@ public void customize(AutoConfigurationCustomizer autoConfiguration) { AgentLog.addSpanLoggingIfRequired(providerBuilder, properties); return providerBuilder; }); + ConfigLogger.triggerInitialLogConfig(); } private void configureExporterUserAgentHeaders(AutoConfigurationCustomizer autoConfiguration) { diff --git a/custom/src/main/java/co/elastic/otel/dynamicconfig/BlockableLogRecordExporter.java b/custom/src/main/java/co/elastic/otel/dynamicconfig/BlockableLogRecordExporter.java index 3c387340..cf35eccc 100644 --- a/custom/src/main/java/co/elastic/otel/dynamicconfig/BlockableLogRecordExporter.java +++ b/custom/src/main/java/co/elastic/otel/dynamicconfig/BlockableLogRecordExporter.java @@ -74,4 +74,14 @@ public CompletableResultCode shutdown() { public void close() { delegate.close(); } + + @Override + public String toString() { + return "BlockableLogRecordExporter{" + + "sendingLogs=" + + sendingLogs + + ", delegate=" + + delegate + + '}'; + } } diff --git a/custom/src/main/java/co/elastic/otel/dynamicconfig/BlockableMetricExporter.java b/custom/src/main/java/co/elastic/otel/dynamicconfig/BlockableMetricExporter.java index 7785594e..4ee7869c 100644 --- a/custom/src/main/java/co/elastic/otel/dynamicconfig/BlockableMetricExporter.java +++ b/custom/src/main/java/co/elastic/otel/dynamicconfig/BlockableMetricExporter.java @@ -87,4 +87,14 @@ public CompletableResultCode shutdown() { public void close() { delegate.close(); } + + @Override + public String toString() { + return "BlockableMetricExporter{" + + "sendingMetrics=" + + sendingMetrics + + ", delegate=" + + delegate + + '}'; + } } diff --git a/custom/src/main/java/co/elastic/otel/dynamicconfig/BlockableSpanExporter.java b/custom/src/main/java/co/elastic/otel/dynamicconfig/BlockableSpanExporter.java index 93d7f8f8..f09f2167 100644 --- a/custom/src/main/java/co/elastic/otel/dynamicconfig/BlockableSpanExporter.java +++ b/custom/src/main/java/co/elastic/otel/dynamicconfig/BlockableSpanExporter.java @@ -69,4 +69,14 @@ public CompletableResultCode flush() { public CompletableResultCode shutdown() { return delegate.shutdown(); } + + @Override + public String toString() { + return "BlockableSpanExporter{" + + "sendingSpans=" + + sendingSpans + + ", delegate=" + + delegate + + '}'; + } } diff --git a/custom/src/main/java/co/elastic/otel/dynamicconfig/CentralConfig.java b/custom/src/main/java/co/elastic/otel/dynamicconfig/CentralConfig.java index 2b2bf9b1..dd078f62 100644 --- a/custom/src/main/java/co/elastic/otel/dynamicconfig/CentralConfig.java +++ b/custom/src/main/java/co/elastic/otel/dynamicconfig/CentralConfig.java @@ -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; }); diff --git a/custom/src/main/java/co/elastic/otel/dynamicconfig/ConfigLogger.java b/custom/src/main/java/co/elastic/otel/dynamicconfig/ConfigLogger.java new file mode 100644 index 00000000..fa0937d0 --- /dev/null +++ b/custom/src/main/java/co/elastic/otel/dynamicconfig/ConfigLogger.java @@ -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); + } 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); + } +} diff --git a/custom/src/main/java/co/elastic/otel/dynamicconfig/DynamicConfiguration.java b/custom/src/main/java/co/elastic/otel/dynamicconfig/DynamicConfiguration.java index e2c928f8..d5833f77 100644 --- a/custom/src/main/java/co/elastic/otel/dynamicconfig/DynamicConfiguration.java +++ b/custom/src/main/java/co/elastic/otel/dynamicconfig/DynamicConfiguration.java @@ -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 + '}'; + } } } diff --git a/custom/src/test/java/co/elastic/otel/dynamicconfig/ConfigLoggerTest.java b/custom/src/test/java/co/elastic/otel/dynamicconfig/ConfigLoggerTest.java new file mode 100644 index 00000000..acab69c1 --- /dev/null +++ b/custom/src/test/java/co/elastic/otel/dynamicconfig/ConfigLoggerTest.java @@ -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"); + } + return jar[0].getAbsolutePath(); + } + + static List createTestTargetCommand() throws IOException { + List 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 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(); + } +} diff --git a/custom/src/test/java/co/elastic/otel/config/DynamicInstrumentationTest.java b/custom/src/test/java/co/elastic/otel/dynamicconfig/DynamicInstrumentationTest.java similarity index 98% rename from custom/src/test/java/co/elastic/otel/config/DynamicInstrumentationTest.java rename to custom/src/test/java/co/elastic/otel/dynamicconfig/DynamicInstrumentationTest.java index dbc48589..4ae6f9f7 100644 --- a/custom/src/test/java/co/elastic/otel/config/DynamicInstrumentationTest.java +++ b/custom/src/test/java/co/elastic/otel/dynamicconfig/DynamicInstrumentationTest.java @@ -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; diff --git a/internal-logging/src/main/java/co/elastic/otel/logging/AgentLog.java b/internal-logging/src/main/java/co/elastic/otel/logging/AgentLog.java index d40c6b16..464f5f14 100644 --- a/internal-logging/src/main/java/co/elastic/otel/logging/AgentLog.java +++ b/internal-logging/src/main/java/co/elastic/otel/logging/AgentLog.java @@ -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 + '}'; + } } }