diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterPushgatewayProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterPushgatewayProperties.java index 9d7b380d1..bd53f8b76 100644 --- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterPushgatewayProperties.java +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterPushgatewayProperties.java @@ -1,5 +1,6 @@ package io.prometheus.metrics.config; +import java.time.Duration; import java.util.Map; import javax.annotation.Nullable; @@ -9,21 +10,29 @@ public class ExporterPushgatewayProperties { private static final String JOB = "job"; private static final String SCHEME = "scheme"; private static final String ESCAPING_SCHEME = "escapingScheme"; + private static final String READ_TIMEOUT = "readTimeoutSeconds"; + private static final String CONNECT_TIMEOUT = "connectTimeoutSeconds"; private static final String PREFIX = "io.prometheus.exporter.pushgateway"; @Nullable private final String scheme; @Nullable private final String address; @Nullable private final String job; @Nullable private final EscapingScheme escapingScheme; + @Nullable private final Duration connectTimeout; + @Nullable private final Duration readTimeout; private ExporterPushgatewayProperties( @Nullable String address, @Nullable String job, @Nullable String scheme, - @Nullable EscapingScheme escapingScheme) { + @Nullable EscapingScheme escapingScheme, + @Nullable Duration connectTimeout, + @Nullable Duration readTimeout) { this.address = address; this.job = job; this.scheme = scheme; this.escapingScheme = escapingScheme; + this.connectTimeout = connectTimeout; + this.readTimeout = readTimeout; } /** Address of the Pushgateway in the form {@code host:port}. Default is {@code localhost:9091} */ @@ -56,6 +65,18 @@ public EscapingScheme getEscapingScheme() { return escapingScheme; } + /** Connection timeout for connections to the Pushgateway. */ + @Nullable + public Duration getConnectTimeout() { + return connectTimeout; + } + + /** Read timeout for connections to the Pushgateway. */ + @Nullable + public Duration getReadTimeout() { + return readTimeout; + } + /** * Note that this will remove entries from {@code properties}. This is because we want to know if * there are unused properties remaining after all properties have been loaded. @@ -66,6 +87,8 @@ static ExporterPushgatewayProperties load(Map properties) String job = Util.loadString(PREFIX + "." + JOB, properties); String scheme = Util.loadString(PREFIX + "." + SCHEME, properties); String escapingScheme = Util.loadString(PREFIX + "." + ESCAPING_SCHEME, properties); + Duration connectTimeout = Util.loadOptionalDuration(PREFIX + "." + CONNECT_TIMEOUT, properties); + Duration readTimeout = Util.loadOptionalDuration(PREFIX + "." + READ_TIMEOUT, properties); if (scheme != null) { if (!scheme.equals("http") && !scheme.equals("https")) { @@ -77,7 +100,7 @@ static ExporterPushgatewayProperties load(Map properties) } return new ExporterPushgatewayProperties( - address, job, scheme, parseEscapingScheme(escapingScheme)); + address, job, scheme, parseEscapingScheme(escapingScheme), connectTimeout, readTimeout); } private static @Nullable EscapingScheme parseEscapingScheme(@Nullable String scheme) { @@ -111,6 +134,8 @@ public static class Builder { @Nullable private String job; @Nullable private String scheme; @Nullable private EscapingScheme escapingScheme; + @Nullable private Duration connectTimeout; + @Nullable private Duration readTimeout; private Builder() {} @@ -134,8 +159,19 @@ public Builder escapingScheme(EscapingScheme escapingScheme) { return this; } + public Builder connectTimeout(Duration connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + public Builder readTimeout(Duration readTimeout) { + this.readTimeout = readTimeout; + return this; + } + public ExporterPushgatewayProperties build() { - return new ExporterPushgatewayProperties(address, job, scheme, escapingScheme); + return new ExporterPushgatewayProperties( + address, job, scheme, escapingScheme, connectTimeout, readTimeout); } } } diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/Util.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/Util.java index 75f0b721e..04eb78a19 100644 --- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/Util.java +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/Util.java @@ -1,5 +1,6 @@ package io.prometheus.metrics.config; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -160,14 +161,30 @@ static Long loadLong(String name, Map properties) return null; } + @Nullable + static Duration loadOptionalDuration(String name, Map properties) + throws PrometheusPropertiesException { + + Long value = loadLong(name, properties); + + assertValue(value, t -> t >= 0, "Expecting value >= 0.", null, name); + + if (value == null || value == 0) { + return null; + } + return Duration.ofSeconds(value); + } + static void assertValue( - @Nullable T number, Predicate predicate, String message, String prefix, String name) + @Nullable T number, + Predicate predicate, + String message, + @Nullable String prefix, + String name) throws PrometheusPropertiesException { if (number != null && !predicate.test(number)) { - String fullMessage = - prefix == null - ? name + ": " + message - : String.format("%s.%s: %s Found: %s", prefix, name, message, number); + String fullKey = prefix == null ? name : prefix + "." + name; + String fullMessage = String.format("%s: %s Found: %s", fullKey, message, number); throw new PrometheusPropertiesException(fullMessage); } } diff --git a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPushgatewayPropertiesTest.java b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPushgatewayPropertiesTest.java index 945fa83be..e1e06348a 100644 --- a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPushgatewayPropertiesTest.java +++ b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPushgatewayPropertiesTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import java.time.Duration; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; @@ -41,11 +42,15 @@ void builder() { .job("job") .scheme("http") .escapingScheme(EscapingScheme.DOTS_ESCAPING) + .connectTimeout(Duration.ofSeconds(1)) + .readTimeout(Duration.ofSeconds(2)) .build(); assertThat(properties.getAddress()).isEqualTo("http://localhost"); assertThat(properties.getJob()).isEqualTo("job"); assertThat(properties.getScheme()).isEqualTo("http"); assertThat(properties.getEscapingScheme()).isEqualTo(EscapingScheme.DOTS_ESCAPING); + assertThat(properties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(1)); + assertThat(properties.getReadTimeout()).isEqualTo(Duration.ofSeconds(2)); } } diff --git a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/UtilTest.java b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/UtilTest.java new file mode 100644 index 000000000..23ffbd33b --- /dev/null +++ b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/UtilTest.java @@ -0,0 +1,50 @@ +package io.prometheus.metrics.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class UtilTest { + @Test + void loadOptionalDuration_positive() { + Map properties = new HashMap<>(Map.of("foo", "5")); + + assertThat(Util.loadOptionalDuration("foo", properties)).isEqualTo(Duration.ofSeconds(5)); + } + + @Test + void loadOptionalDuration_zero() { + Map properties = new HashMap<>(Map.of("foo", "0")); + + assertThat(Util.loadOptionalDuration("foo", properties)).isNull(); + } + + @Test + void loadOptionalDuration_missing() { + Map properties = new HashMap<>(); + + assertThat(Util.loadOptionalDuration("foo", properties)).isNull(); + } + + @Test + void loadOptionalDuration_negative_throws() { + Map properties = new HashMap<>(Map.of("foo", "-1")); + + assertThatExceptionOfType(PrometheusPropertiesException.class) + .isThrownBy(() -> Util.loadOptionalDuration("foo", properties)) + .withMessage("foo: Expecting value >= 0. Found: -1"); + } + + @Test + void loadOptionalDuration_invalidNumber_throws() { + Map properties = new HashMap<>(Map.of("foo", "abc")); + + assertThatExceptionOfType(PrometheusPropertiesException.class) + .isThrownBy(() -> Util.loadOptionalDuration("foo", properties)) + .withMessage("foo=abc: Expecting long value"); + } +} diff --git a/prometheus-metrics-exporter-pushgateway/src/main/java/io/prometheus/metrics/exporter/pushgateway/PushGateway.java b/prometheus-metrics-exporter-pushgateway/src/main/java/io/prometheus/metrics/exporter/pushgateway/PushGateway.java index 643e0aeca..72e0561c1 100644 --- a/prometheus-metrics-exporter-pushgateway/src/main/java/io/prometheus/metrics/exporter/pushgateway/PushGateway.java +++ b/prometheus-metrics-exporter-pushgateway/src/main/java/io/prometheus/metrics/exporter/pushgateway/PushGateway.java @@ -27,6 +27,7 @@ import java.net.URLEncoder; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.Base64; import java.util.Collections; import java.util.HashMap; @@ -79,9 +80,6 @@ * href="https://github.com/prometheus/pushgateway">https://github.com/prometheus/pushgateway. */ public class PushGateway { - - private static final int MILLISECONDS_PER_SECOND = 1000; - private final URL url; private final ExpositionFormatWriter writer; private final boolean prometheusTimestampsInMs; @@ -89,6 +87,8 @@ public class PushGateway { private final PrometheusRegistry registry; private final HttpConnectionFactory connectionFactory; private final EscapingScheme escapingScheme; + private final Duration connectionTimeout; + private final Duration readTimeout; private PushGateway( PrometheusRegistry registry, @@ -97,13 +97,17 @@ private PushGateway( HttpConnectionFactory connectionFactory, Map requestHeaders, boolean prometheusTimestampsInMs, - EscapingScheme escapingScheme) { + EscapingScheme escapingScheme, + Duration connectionTimeout, + Duration readTimeout) { this.registry = registry; this.url = url; this.requestHeaders = Collections.unmodifiableMap(new HashMap<>(requestHeaders)); this.connectionFactory = connectionFactory; this.prometheusTimestampsInMs = prometheusTimestampsInMs; this.escapingScheme = escapingScheme; + this.connectionTimeout = connectionTimeout; + this.readTimeout = readTimeout; writer = getWriter(format); if (!writer.isAvailable()) { throw new RuntimeException(writer.getClass() + " is not available"); @@ -206,8 +210,8 @@ private void doRequest(@Nullable PrometheusRegistry registry, String method) thr } connection.setRequestMethod(method); - connection.setConnectTimeout(10 * MILLISECONDS_PER_SECOND); - connection.setReadTimeout(10 * MILLISECONDS_PER_SECOND); + connection.setConnectTimeout((int) this.connectionTimeout.toMillis()); + connection.setReadTimeout((int) this.readTimeout.toMillis()); connection.connect(); try { @@ -277,6 +281,8 @@ public static class Builder { @Nullable private String address; @Nullable private Scheme scheme; @Nullable private String job; + @Nullable private Duration connectionTimeout; + @Nullable private Duration readTimeout; private boolean prometheusTimestampsInMs; private final Map requestHeaders = new HashMap<>(); private PrometheusRegistry registry = PrometheusRegistry.defaultRegistry; @@ -395,6 +401,49 @@ public Builder prometheusTimestampsInMs(boolean prometheusTimestampsInMs) { return this; } + /** + * Specify the connection timeout for HTTP connections to the PushGateway. Default is 10 + * seconds. + * + * @param connectionTimeout timeout value + * @return this {@link Builder} instance + */ + public Builder connectionTimeout(Duration connectionTimeout) { + this.connectionTimeout = connectionTimeout; + return this; + } + + private Duration getConnectionTimeout(@Nullable ExporterPushgatewayProperties properties) { + if (properties != null && properties.getConnectTimeout() != null) { + return properties.getConnectTimeout(); + } else if (this.connectionTimeout != null) { + return this.connectionTimeout; + } else { + return Duration.ofSeconds(10); + } + } + + /** + * Specify the read timeout for HTTP connections to the PushGateway. Default is 10 seconds. + * + * @param readTimeout timeout value + * @return this {@link Builder} instance + */ + public Builder readTimeout(Duration readTimeout) { + this.readTimeout = readTimeout; + return this; + } + + private Duration getReadTimeout(@Nullable ExporterPushgatewayProperties properties) { + if (properties != null && properties.getReadTimeout() != null) { + return properties.getReadTimeout(); + } else if (this.readTimeout != null) { + return this.readTimeout; + } else { + return Duration.ofSeconds(10); + } + } + private boolean getPrometheusTimestampsInMs() { // accept either to opt in to timestamps in milliseconds return config.getExporterProperties().getPrometheusTimestampsInMs() @@ -496,7 +545,9 @@ public PushGateway build() { connectionFactory, requestHeaders, getPrometheusTimestampsInMs(), - getEscapingScheme(properties)); + getEscapingScheme(properties), + getConnectionTimeout(properties), + getReadTimeout(properties)); } catch (MalformedURLException e) { throw new PrometheusPropertiesException( address + ": Invalid address. Expecting :");