diff --git a/apm-agent-api/src/main/java/co/elastic/apm/api/ElasticApm.java b/apm-agent-api/src/main/java/co/elastic/apm/api/ElasticApm.java index 0610be92b4..7de78b7493 100644 --- a/apm-agent-api/src/main/java/co/elastic/apm/api/ElasticApm.java +++ b/apm-agent-api/src/main/java/co/elastic/apm/api/ElasticApm.java @@ -249,4 +249,62 @@ public static void captureException(@Nullable Throwable e) { public static void setServiceInfoForClassLoader(@Nullable ClassLoader classLoader, @Nullable String serviceName, @Nullable String serviceVersion) { // co.elastic.apm.api.ElasticApmInstrumentation.SetServiceInfoForClassLoader.setServiceInfoForClassLoader } + + /** + * Returns the current value of a config option (regardless of however and whenever that option was (re)set) + *
+ * This applies to dynamically updated values too, so for example "transaction_sample_rate" + * can be dynamically reset (eg by executing + * System.setProperty("elastic.apm.transaction_sample_rate", "0.6") + * or changing the "transaction_sample_rate" in the property file or from fleet UI) + * and calling getConfig("transaction_sample_rate") would provide the current value + *
+ *+ * If an invalid key is passed as the first argument, or if the incorrect type for + * the key is passed as the second argument, the method will throw an IllegalArgumentException + *
+ *+ * Note the value returned can be null if the "public-api" set of instrumentations + * has not been applied and also if the agent hasn't finished initializing + *
+ * + * @param key the string option + * @param type the type of the option (String, Long, Integer, etc) + * @return The current value for the option, per the option type (String, Long, Integer, etc) + * @since 1.37.0 + */ + public static+ * Note there is a delay between executing the update and it taking effect. The corresponding + * {@link #getConfig(String, Class)} ()} will only report the new value after it has taken effect. + * Note also that only dynamic options can be updated, other updates will simply be ignored. + *
+ * + * @param key the string option + * @param value the value for that option. No validation is applied here, so invalid + * values for an option will be ignored by the configuration system + * @since 1.37.0 + */ + public static void setConfig(String key, String value) { + System.setProperty("elastic.apm."+key, value); + } } diff --git a/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/pluginapi/ElasticApmApiInstrumentation.java b/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/pluginapi/ElasticApmApiInstrumentation.java index c7cb07f4be..841adfbed4 100644 --- a/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/pluginapi/ElasticApmApiInstrumentation.java +++ b/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/pluginapi/ElasticApmApiInstrumentation.java @@ -19,12 +19,14 @@ package co.elastic.apm.agent.pluginapi; import co.elastic.apm.agent.configuration.ServiceInfo; +import co.elastic.apm.agent.impl.GlobalTracer; import co.elastic.apm.agent.impl.transaction.Transaction; import co.elastic.apm.agent.util.PrivilegedActionUtils; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; +import org.stagemonitor.configuration.ConfigurationOption; import javax.annotation.Nullable; import java.lang.invoke.MethodHandle; @@ -162,4 +164,29 @@ public static void setServiceInfoForClassLoader(@Advice.Argument(0) @Nullable Cl } } } + + public static class ConfigInstrumentation extends ElasticApmApiInstrumentation { + public ConfigInstrumentation() { + super(named("doGetConfig")); + } + + public static class AdviceClass { + @Nullable + @Advice.AssignReturned.ToReturned + @Advice.OnMethodExit(suppress = Throwable.class, inline = false) + public static Object doGetConfig(@Advice.Argument(0) @Nullable String key) { + try { + ConfigurationOption> configValue = GlobalTracer.getTracerImpl().getConfigurationRegistry().getConfigurationOptionByKey(key); + if (configValue == null) { + return null; + } else { + return configValue.getValue(); + } + } catch (NullPointerException e) { + //this can only happen if the agent is messed up or not yet initialized + return new IllegalStateException("The agent is not initialized"); + } + } + } + } } diff --git a/apm-agent-plugins/apm-api-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation b/apm-agent-plugins/apm-api-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation index 4497cb5f29..f117dfa6a8 100644 --- a/apm-agent-plugins/apm-api-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation +++ b/apm-agent-plugins/apm-api-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation @@ -4,6 +4,7 @@ co.elastic.apm.agent.pluginapi.ElasticApmApiInstrumentation$CurrentTransactionIn co.elastic.apm.agent.pluginapi.ElasticApmApiInstrumentation$CurrentSpanInstrumentation co.elastic.apm.agent.pluginapi.ElasticApmApiInstrumentation$CaptureExceptionInstrumentation co.elastic.apm.agent.pluginapi.ElasticApmApiInstrumentation$SetServiceInfoForClassLoaderInstrumentation +co.elastic.apm.agent.pluginapi.ElasticApmApiInstrumentation$ConfigInstrumentation co.elastic.apm.agent.pluginapi.TransactionInstrumentation$SetFrameworkNameInstrumentation co.elastic.apm.agent.pluginapi.TransactionInstrumentation$SetUserInstrumentation co.elastic.apm.agent.pluginapi.TransactionInstrumentation$EnsureParentIdInstrumentation diff --git a/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/agent/pluginapi/ConfigInstrumentationTest.java b/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/agent/pluginapi/ConfigInstrumentationTest.java new file mode 100644 index 0000000000..12690a19b3 --- /dev/null +++ b/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/agent/pluginapi/ConfigInstrumentationTest.java @@ -0,0 +1,135 @@ +/* + * 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.apm.agent.pluginapi; + +import co.elastic.apm.agent.MockTracer; +import co.elastic.apm.agent.bci.ElasticApmAgent; +import co.elastic.apm.agent.bci.TracerAwareInstrumentation; +import co.elastic.apm.agent.configuration.CoreConfiguration; +import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.sdk.ElasticApmInstrumentation; +import co.elastic.apm.api.ElasticApm; +import net.bytebuddy.agent.ByteBuddyAgent; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.stagemonitor.configuration.ConfigurationRegistry; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatException; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +class ConfigInstrumentationTest { + + private static final Integer INVALID_TRANSACTION_MAX_SPANS = -37; + + private ElasticApmTracer tracer; + private ConfigurationRegistry configurationRegistry; + private CoreConfiguration coreConfig; + + @BeforeEach + void setup() { + tracer = MockTracer.createRealTracer(); + configurationRegistry = tracer.getConfigurationRegistry(); + coreConfig = configurationRegistry.getConfig(CoreConfiguration.class); + } + + @AfterEach + void reset() { + ElasticApmAgent.reset(); + } + + @Test + void testValidOptionConfigAccess() { + initAgent(); + assertThat(configValue(Boolean.TRUE)).isEqualTo(coreConfig.getTransactionMaxSpans()); + } + + @Test + void testInvalidOptionConfigAccess() { + initAgent(); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> configValue(Boolean.FALSE)) + .withMessageStartingWith("There is no such option"); + } + + @Test + void testInvalidTypeConfigAccess() { + initAgent(); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> configValue(null)) + .withMessageContaining("is not of type"); + } + + private void initAgent() { + ArrayList