diff --git a/custom/build.gradle.kts b/custom/build.gradle.kts index d75f7807..4c4df17a 100644 --- a/custom/build.gradle.kts +++ b/custom/build.gradle.kts @@ -75,7 +75,11 @@ tasks { } } -tasks.withType { +tasks.withType().configureEach { + val agentJarTask = project(":agent").tasks.named("jar") + dependsOn(agentJarTask) + systemProperty("elastic.otel.agent.jar.path", agentJarTask.get().archiveFile.get().asFile.absolutePath) + val overrideConfig = project.properties["elastic.otel.overwrite.config.docs"] if (overrideConfig != null) { systemProperty("elastic.otel.overwrite.config.docs", overrideConfig) diff --git a/custom/src/main/java/co/elastic/otel/ElasticAutoConfigurationCustomizerProvider.java b/custom/src/main/java/co/elastic/otel/ElasticAutoConfigurationCustomizerProvider.java index 62c74f21..a11bfdb1 100644 --- a/custom/src/main/java/co/elastic/otel/ElasticAutoConfigurationCustomizerProvider.java +++ b/custom/src/main/java/co/elastic/otel/ElasticAutoConfigurationCustomizerProvider.java @@ -19,6 +19,7 @@ package co.elastic.otel; import co.elastic.otel.compositesampling.DynamicCompositeParentBasedTraceIdRatioBasedSampler; +import co.elastic.otel.config.ConfigLoggingAgentListener; import co.elastic.otel.dynamicconfig.BlockableLogRecordExporter; import co.elastic.otel.dynamicconfig.BlockableMetricExporter; import co.elastic.otel.dynamicconfig.BlockableSpanExporter; @@ -104,6 +105,8 @@ static Map propertiesCustomizer(ConfigProperties configPropertie deltaMetricsTemporality(config, configProperties); resourceProviders(config, configProperties); spanStackTrace(config, configProperties); + ConfigLoggingAgentListener.logTheConfig( + configProperties.getBoolean(ConfigLoggingAgentListener.LOG_THE_CONFIG, true)); return config; } diff --git a/custom/src/main/java/co/elastic/otel/compositesampling/DynamicCompositeParentBasedTraceIdRatioBasedSampler.java b/custom/src/main/java/co/elastic/otel/compositesampling/DynamicCompositeParentBasedTraceIdRatioBasedSampler.java index 3304202b..6135135f 100644 --- a/custom/src/main/java/co/elastic/otel/compositesampling/DynamicCompositeParentBasedTraceIdRatioBasedSampler.java +++ b/custom/src/main/java/co/elastic/otel/compositesampling/DynamicCompositeParentBasedTraceIdRatioBasedSampler.java @@ -32,7 +32,8 @@ public enum DynamicCompositeParentBasedTraceIdRatioBasedSampler implements Sampl static final double DEFAULT_TRACEIDRATIO_SAMPLE_RATIO = 1.0d; - private static Sampler delegate = newSampler(DEFAULT_TRACEIDRATIO_SAMPLE_RATIO); + private static volatile double latestRatio; + private static volatile Sampler delegate = newSampler(DEFAULT_TRACEIDRATIO_SAMPLE_RATIO); public static void setRatio(double ratio) { delegate = newSampler(ratio); @@ -55,6 +56,15 @@ public String getDescription() { } private static Sampler newSampler(double ratio) { + latestRatio = ratio; return ConsistentSampler.parentBased(ConsistentSampler.probabilityBased(ratio)); } + + public String toString() { + return "DynamicCompositeParentBasedTraceIdRatioBasedSampler(ratio=" + + latestRatio + + ", " + + delegate.toString() + + ")"; + } } diff --git a/custom/src/main/java/co/elastic/otel/config/ConfigLoggingAgentListener.java b/custom/src/main/java/co/elastic/otel/config/ConfigLoggingAgentListener.java new file mode 100644 index 00000000..1029e399 --- /dev/null +++ b/custom/src/main/java/co/elastic/otel/config/ConfigLoggingAgentListener.java @@ -0,0 +1,49 @@ +/* + * 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.config; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.AgentListener; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import java.util.logging.Logger; + +@AutoService(AgentListener.class) +public class ConfigLoggingAgentListener implements AgentListener { + public static final String LOG_THE_CONFIG = + "elastic.otel.java.experimental.configuration.logging.enabled"; + private static final Logger logger = Logger.getLogger(ConfigLoggingAgentListener.class.getName()); + + private static boolean logTheConfig = true; + + public static synchronized void logTheConfig(boolean logTheConfig) { + ConfigLoggingAgentListener.logTheConfig = logTheConfig; + } + + @Override + public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) { + if (logTheConfig) { + logger.info(autoConfiguredOpenTelemetrySdk.toString()); + } + } + + @Override + public int order() { + return 1; + } +} diff --git a/custom/src/test/java/co/elastic/otel/config/ConfigLoggingAgentListenerTest.java b/custom/src/test/java/co/elastic/otel/config/ConfigLoggingAgentListenerTest.java new file mode 100644 index 00000000..fe4b87da --- /dev/null +++ b/custom/src/test/java/co/elastic/otel/config/ConfigLoggingAgentListenerTest.java @@ -0,0 +1,163 @@ +/* + * 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.config; + +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.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class ConfigLoggingAgentListenerTest { + static final String TARGET_CLASS_NAME = "TempTest321"; + static String[] identifyingStrings = { + "ConfigLoggingAgentListener - AutoConfiguredOpenTelemetrySdk{", + "tracerProvider=SdkTracerProvider" + }; + + private static String agentJarFile; + private static File testTargetClass; + + @BeforeAll + public static void setup() throws IOException { + agentJarFile = getAgentJarFile(); + testTargetClass = createTestTarget(); + } + + @AfterAll + public static void teardown() throws IOException { + testTargetClass.delete(); + } + + @Test + public void checkLogConfigPresent() throws IOException { + String output = executeCommand(createTestTargetCommand(true), 20); + for (String identifyingString : identifyingStrings) { + assertThat(output).contains(identifyingString); + } + } + + @Test + public void checkLogConfigAbsent() throws IOException { + String output = executeCommand(createTestTargetCommand(false), 20); + for (String identifyingString : identifyingStrings) { + assertThat(output).doesNotContain(identifyingString); + } + } + + static List createTestTargetCommand(boolean logConfig) throws IOException { + List command = new ArrayList<>(); + command.add("java"); + command.add("-Xmx32m"); + command.add("-javaagent:" + agentJarFile); + // Only on false, ie test the 'true' default with no option + if (!logConfig) { + command.add("-D" + ConfigLoggingAgentListener.LOG_THE_CONFIG + "=false"); + } + command.add(testTargetClass.getAbsolutePath()); + return command; + } + + 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" + // 15 seconds to give the agent plenty of time to start and be fully initialized + + " try {Thread.sleep(15_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() { + String path = System.getProperty("elastic.otel.agent.jar.path"); + if (path == null) { + throw new IllegalStateException("elastic.otel.agent.jar.path system property is not set"); + } + return path; + } + + 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(); + } + + private static void pauseSeconds(int seconds) { + try { + Thread.sleep(seconds * 1_000L); + } catch (InterruptedException e) { + } + } +}