diff --git a/CHANGELOG.md b/CHANGELOG.md index e7b45d02c6..1691904b64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Features * Add support for Spring's JMS flavor - instrumenting `org.springframework.jms.listener.SessionAwareMessageListener` * Add support to legacy ApacheHttpClient APIs (which adds support to Axis2 configured to use ApacheHttpClient) + * Added support for setting `server_urls` dynamically via properties file [#723](https://github.com/elastic/apm-agent-java/issues/723) ## Bug Fixes * Some JMS Consumers and Producers are filtered due to class name filtering in instrumentation matching diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ApmServerClient.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ApmServerClient.java index 6bde86883d..955da32deb 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ApmServerClient.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ApmServerClient.java @@ -27,6 +27,7 @@ import co.elastic.apm.agent.util.VersionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.stagemonitor.configuration.ConfigurationOption; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -64,20 +65,29 @@ public class ApmServerClient { private static final Logger logger = LoggerFactory.getLogger(ApmServerClient.class); private static final String USER_AGENT = "elasticapm-java/" + VersionUtils.getAgentVersion(); private final ReporterConfiguration reporterConfiguration; - private final List serverUrls; + private volatile List serverUrls; private final AtomicInteger errorCount = new AtomicInteger(); public ApmServerClient(ReporterConfiguration reporterConfiguration) { - this(reporterConfiguration, shuffleUrls(reporterConfiguration)); + this(reporterConfiguration, shuffleUrls(reporterConfiguration.getServerUrls())); } - public ApmServerClient(ReporterConfiguration reporterConfiguration, List serverUrls) { + public ApmServerClient(ReporterConfiguration reporterConfiguration, List shuffledUrls) { this.reporterConfiguration = reporterConfiguration; - this.serverUrls = Collections.unmodifiableList(serverUrls); + this.reporterConfiguration.getServerUrlsOption().addChangeListener(new ConfigurationOption.ChangeListener>() { + @Override + public void onChange(ConfigurationOption configurationOption, List oldValue, List newValue) { + logger.debug("server_urls override with value = ({}).", newValue); + if (newValue != null && !newValue.isEmpty()) { + serverUrls = shuffleUrls(newValue); + errorCount.set(0); + } + } + }); + this.serverUrls = Collections.unmodifiableList(shuffledUrls); } - private static List shuffleUrls(ReporterConfiguration reporterConfiguration) { - List serverUrls = new ArrayList<>(reporterConfiguration.getServerUrls()); + private static List shuffleUrls(List serverUrls) { // shuffling the URL list helps to distribute the load across the apm servers // when there are multiple agents, they should not all start connecting to the same apm server Collections.shuffle(serverUrls); @@ -140,6 +150,7 @@ private URL appendPath(URL serverUrl, String apmServerPath) throws MalformedURLE * the error count is not incremented. * This avoids concurrent requests from incrementing the error multiple times due to only one failing server. *

+ * * @param expectedErrorCount the error count that is expected by the current thread * @return the new expected error count */ @@ -240,6 +251,10 @@ int getErrorCount() { return errorCount.get(); } + List getServerUrls() { + return this.serverUrls; + } + public interface ConnectionHandler { @Nullable T withConnection(HttpURLConnection connection) throws IOException; diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ReporterConfiguration.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ReporterConfiguration.java index 9f43542b8e..c2eae0c041 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ReporterConfiguration.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ReporterConfiguration.java @@ -11,9 +11,9 @@ * 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 @@ -68,7 +68,7 @@ public class ReporterConfiguration extends ConfigurationOptionProvider { "If outgoing HTTP traffic has to go through a proxy," + "you can use the Java system properties `http.proxyHost` and `http.proxyPort` to set that up.\n" + "See also [Java's proxy documentation](https://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html) for more information.") - .dynamic(false) + .dynamic(true) .buildWithDefault(Collections.singletonList(UrlValueConverter.INSTANCE.convert("http://localhost:8200"))); private final ConfigurationOption serverTimeout = TimeDurationValueConverter.durationOption("s") @@ -212,4 +212,9 @@ public long getMetricsIntervalMs() { public List getDisableMetrics() { return disableMetrics.get(); } + + public ConfigurationOption> getServerUrlsOption() { + return this.serverUrl; + } + } diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/report/ApmServerClientTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/report/ApmServerClientTest.java index e01c412ebc..118efc86bf 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/report/ApmServerClientTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/report/ApmServerClientTest.java @@ -11,9 +11,9 @@ * 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 @@ -24,17 +24,24 @@ */ package co.elastic.apm.agent.report; +import co.elastic.apm.agent.configuration.SpyConfiguration; +import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.impl.ElasticApmTracerBuilder; import com.github.tomakehurst.wiremock.core.WireMockConfiguration; import com.github.tomakehurst.wiremock.junit.WireMockRule; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.mockito.Mockito; +import org.stagemonitor.configuration.ConfigurationRegistry; import java.io.FileNotFoundException; +import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; +import java.util.Arrays; import java.util.List; import static com.github.tomakehurst.wiremock.client.WireMock.get; @@ -52,17 +59,27 @@ public class ApmServerClientTest { @Rule public WireMockRule apmServer2 = new WireMockRule(WireMockConfiguration.wireMockConfig().dynamicPort()); private ApmServerClient apmServerClient; + private ConfigurationRegistry config; + private ElasticApmTracer tracer; + private ReporterConfiguration reporterConfiguration; @Before public void setUp() throws MalformedURLException { + URL url1 = new URL("http", "localhost", apmServer1.port(), "/"); + URL url2 = new URL("http", "localhost", apmServer2.port(), "/"); + config = SpyConfiguration.createSpyConfig(); + tracer = new ElasticApmTracerBuilder() + .configurationRegistry(config) + .build(); + reporterConfiguration = tracer.getConfig(ReporterConfiguration.class); + + Mockito.when(reporterConfiguration.getServerUrls()).thenReturn(Arrays.asList(url1, url2)); + apmServer1.stubFor(get(urlEqualTo("/test")).willReturn(notFound())); apmServer1.stubFor(get(urlEqualTo("/not-found")).willReturn(notFound())); apmServer2.stubFor(get(urlEqualTo("/test")).willReturn(ok("hello from server 2"))); apmServer2.stubFor(get(urlEqualTo("/not-found")).willReturn(notFound())); - apmServerClient = new ApmServerClient(new ReporterConfiguration(), List.of( - new URL("http", "localhost", apmServer1.port(), "/"), - new URL("http", "localhost", apmServer2.port(), "/") - )); + apmServerClient = new ApmServerClient(reporterConfiguration, tracer.getConfig(ReporterConfiguration.class).getServerUrls()); } @Test @@ -148,4 +165,14 @@ public void testSimulateConcurrentConnectionError() { apmServerClient.incrementAndGetErrorCount(0); assertThat(apmServerClient.getErrorCount()).isOne(); } + + @Test + public void testGetServerUrlsVerifyThatServerUrlsWillBeReloaded() throws IOException { + URL tempUrl = new URL("http", "localhost", 9999, ""); + config.save("server_urls", tempUrl.toString(), SpyConfiguration.CONFIG_SOURCE_NAME); + + List updatedServerUrls = apmServerClient.getServerUrls(); + + assertThat(updatedServerUrls).isEqualTo(Arrays.asList(tempUrl)); + } } diff --git a/docs/configuration.asciidoc b/docs/configuration.asciidoc index ef5a31ffe1..fb34dc383c 100644 --- a/docs/configuration.asciidoc +++ b/docs/configuration.asciidoc @@ -882,7 +882,7 @@ See also [Java's proxy documentation](https://docs.oracle.com/javase/8/docs/tech [options="header"] |============ | Default | Type | Dynamic -| `http://localhost:8200` | List | false +| `http://localhost:8200` | List | true |============ @@ -1633,7 +1633,7 @@ The default unit for this option is `ms` # If outgoing HTTP traffic has to go through a proxy,you can use the Java system properties `http.proxyHost` and `http.proxyPort` to set that up. # See also [Java's proxy documentation](https://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html) for more information. # -# This setting can not be changed at runtime. Changes require a restart of the application. +# This setting can be changed at runtime # Type: comma separated list # Default value: http://localhost:8200 #