You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fluss.apache.org by yu...@apache.org on 2026/07/03 08:53:12 UTC

(fluss) branch main updated: [metrics][docs] Support Basic Auth for Prometheus PushGateway reporter (#3552)

This is an automated email from the ASF dual-hosted git repository.

luoyuxia pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss.git


The following commit(s) were added to refs/heads/main by this push:
     new 6238daaed [metrics][docs] Support Basic Auth for Prometheus PushGateway reporter (#3552)
6238daaed is described below

commit 6238daaed660b02b793924569205e50677d378fc
Author: GuoYu <51...@qq.com>
AuthorDate: Fri Jul 3 16:53:08 2026 +0800

    [metrics][docs] Support Basic Auth for Prometheus PushGateway reporter (#3552)
---
 .../org/apache/fluss/config/ConfigOptions.java     |  18 ++
 .../apache/fluss/config/ConfigurationUtils.java    |   4 +
 .../prometheus/PrometheusPushGatewayReporter.java  |  28 ++-
 .../PrometheusPushGatewayReporterPlugin.java       |  20 ++-
 .../PrometheusPushGatewayReporterTest.java         | 190 +++++++++++++++++++++
 website/docs/maintenance/configuration.md          |   2 +
 .../maintenance/observability/metric-reporters.md  |   4 +
 7 files changed, 262 insertions(+), 4 deletions(-)

diff --git a/fluss-common/src/main/java/org/apache/fluss/config/ConfigOptions.java b/fluss-common/src/main/java/org/apache/fluss/config/ConfigOptions.java
index 0301d3d47..2d97ebab5 100644
--- a/fluss-common/src/main/java/org/apache/fluss/config/ConfigOptions.java
+++ b/fluss-common/src/main/java/org/apache/fluss/config/ConfigOptions.java
@@ -2171,6 +2171,24 @@ public class ConfigOptions {
                             .withDescription(
                                     "The interval of pushing metrics to Prometheus PushGateway.");
 
+    public static final ConfigOption<String> METRICS_REPORTER_PROMETHEUS_PUSHGATEWAY_USERNAME =
+            key("metrics.reporter.prometheus-push.username")
+                    .stringType()
+                    .noDefaultValue()
+                    .withDescription(
+                            "The username for Basic Auth of the Prometheus PushGateway. "
+                                    + "Leave it unset to disable authentication.");
+
+    public static final ConfigOption<Password> METRICS_REPORTER_PROMETHEUS_PUSHGATEWAY_PASSWORD =
+            key("metrics.reporter.prometheus-push.password")
+                    .passwordType()
+                    .noDefaultValue()
+                    .withDescription(
+                            "The password for Basic Auth of the Prometheus PushGateway. "
+                                    + "Only takes effect when username is configured. "
+                                    + "The value is automatically redacted when the configuration "
+                                    + "is logged or displayed.");
+
     // ------------------------------------------------------------------------
     //  ConfigOptions for jmx reporter
     // ------------------------------------------------------------------------
diff --git a/fluss-common/src/main/java/org/apache/fluss/config/ConfigurationUtils.java b/fluss-common/src/main/java/org/apache/fluss/config/ConfigurationUtils.java
index 4b4ae09fc..1d0810256 100644
--- a/fluss-common/src/main/java/org/apache/fluss/config/ConfigurationUtils.java
+++ b/fluss-common/src/main/java/org/apache/fluss/config/ConfigurationUtils.java
@@ -99,6 +99,10 @@ public class ConfigurationUtils {
         } else if (String.class.equals(clazz)) {
             return (T) convertToString(rawValue);
         } else if (Password.class.equals(clazz)) {
+            if (rawValue instanceof Password) {
+                return (T) rawValue;
+            }
+
             return (T) new Password(convertToString(rawValue));
         } else if (clazz.isEnum()) {
             return (T) convertToEnum(rawValue, (Class<? extends Enum<?>>) clazz);
diff --git a/fluss-metrics/fluss-metrics-prometheus/src/main/java/org/apache/fluss/metrics/prometheus/PrometheusPushGatewayReporter.java b/fluss-metrics/fluss-metrics-prometheus/src/main/java/org/apache/fluss/metrics/prometheus/PrometheusPushGatewayReporter.java
index f6102f9cd..cfe0b858e 100644
--- a/fluss-metrics/fluss-metrics-prometheus/src/main/java/org/apache/fluss/metrics/prometheus/PrometheusPushGatewayReporter.java
+++ b/fluss-metrics/fluss-metrics-prometheus/src/main/java/org/apache/fluss/metrics/prometheus/PrometheusPushGatewayReporter.java
@@ -19,14 +19,21 @@ package org.apache.fluss.metrics.prometheus;
 
 import org.apache.fluss.metrics.Metric;
 import org.apache.fluss.metrics.reporter.ScheduledMetricReporter;
+import org.apache.fluss.utils.StringUtils;
 
+import io.prometheus.client.exporter.HttpConnectionFactory;
 import io.prometheus.client.exporter.PushGateway;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.annotation.Nullable;
+
 import java.io.IOException;
+import java.net.HttpURLConnection;
 import java.net.URL;
+import java.nio.charset.StandardCharsets;
 import java.time.Duration;
+import java.util.Base64;
 import java.util.Map;
 
 /** {@link ScheduledMetricReporter} that pushes {@link Metric Metrics} to Prometheus PushGateway. */
@@ -46,12 +53,18 @@ public class PrometheusPushGatewayReporter extends AbstractPrometheusReporter
             String jobName,
             Map<String, String> groupingKey,
             final boolean deleteOnShutdown,
-            Duration pushInterval) {
+            Duration pushInterval,
+            @Nullable String username,
+            @Nullable String password) {
         this.pushGateway = new PushGateway(hostUrl);
         this.jobName = jobName;
         this.groupingKey = groupingKey;
         this.deleteOnShutdown = deleteOnShutdown;
         this.pushInterval = pushInterval;
+        if (!StringUtils.isNullOrWhitespaceOnly(username)) {
+            this.pushGateway.setConnectionFactory(
+                    basicAuthConnectionFactory(username, password == null ? "" : password));
+        }
     }
 
     @Override
@@ -80,4 +93,17 @@ public class PrometheusPushGatewayReporter extends AbstractPrometheusReporter
             LOG.warn("Could not push metrics to PushGateway.", e);
         }
     }
+
+    private static HttpConnectionFactory basicAuthConnectionFactory(String user, String password) {
+        final String header =
+                "Basic "
+                        + Base64.getEncoder()
+                                .encodeToString(
+                                        (user + ":" + password).getBytes(StandardCharsets.UTF_8));
+        return url -> {
+            HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
+            connection.setRequestProperty("Authorization", header);
+            return connection;
+        };
+    }
 }
diff --git a/fluss-metrics/fluss-metrics-prometheus/src/main/java/org/apache/fluss/metrics/prometheus/PrometheusPushGatewayReporterPlugin.java b/fluss-metrics/fluss-metrics-prometheus/src/main/java/org/apache/fluss/metrics/prometheus/PrometheusPushGatewayReporterPlugin.java
index 5786a002c..a1b565109 100644
--- a/fluss-metrics/fluss-metrics-prometheus/src/main/java/org/apache/fluss/metrics/prometheus/PrometheusPushGatewayReporterPlugin.java
+++ b/fluss-metrics/fluss-metrics-prometheus/src/main/java/org/apache/fluss/metrics/prometheus/PrometheusPushGatewayReporterPlugin.java
@@ -19,6 +19,7 @@ package org.apache.fluss.metrics.prometheus;
 
 import org.apache.fluss.annotation.VisibleForTesting;
 import org.apache.fluss.config.Configuration;
+import org.apache.fluss.config.Password;
 import org.apache.fluss.metrics.reporter.MetricReporter;
 import org.apache.fluss.metrics.reporter.MetricReporterPlugin;
 import org.apache.fluss.utils.StringUtils;
@@ -37,8 +38,10 @@ import static org.apache.fluss.config.ConfigOptions.METRICS_REPORTER_PROMETHEUS_
 import static org.apache.fluss.config.ConfigOptions.METRICS_REPORTER_PROMETHEUS_PUSHGATEWAY_GROUPING_KEY;
 import static org.apache.fluss.config.ConfigOptions.METRICS_REPORTER_PROMETHEUS_PUSHGATEWAY_HOST_URL;
 import static org.apache.fluss.config.ConfigOptions.METRICS_REPORTER_PROMETHEUS_PUSHGATEWAY_JOB_NAME;
+import static org.apache.fluss.config.ConfigOptions.METRICS_REPORTER_PROMETHEUS_PUSHGATEWAY_PASSWORD;
 import static org.apache.fluss.config.ConfigOptions.METRICS_REPORTER_PROMETHEUS_PUSHGATEWAY_PUSH_INTERVAL;
 import static org.apache.fluss.config.ConfigOptions.METRICS_REPORTER_PROMETHEUS_PUSHGATEWAY_RANDOM_JOB_NAME_SUFFIX;
+import static org.apache.fluss.config.ConfigOptions.METRICS_REPORTER_PROMETHEUS_PUSHGATEWAY_USERNAME;
 
 /** {@link MetricReporterPlugin} for {@link PrometheusPushGatewayReporter}. */
 public class PrometheusPushGatewayReporterPlugin implements MetricReporterPlugin {
@@ -56,23 +59,34 @@ public class PrometheusPushGatewayReporterPlugin implements MetricReporterPlugin
         boolean randomSuffix =
                 config.get(METRICS_REPORTER_PROMETHEUS_PUSHGATEWAY_RANDOM_JOB_NAME_SUFFIX);
         Duration pushInterval = config.get(METRICS_REPORTER_PROMETHEUS_PUSHGATEWAY_PUSH_INTERVAL);
+        String username = config.get(METRICS_REPORTER_PROMETHEUS_PUSHGATEWAY_USERNAME);
+        Password passwordOption = config.get(METRICS_REPORTER_PROMETHEUS_PUSHGATEWAY_PASSWORD);
+        String password = passwordOption == null ? null : passwordOption.value();
         String jobName = configuredJobName;
         if (randomSuffix) {
             jobName = configuredJobName + new Random().nextLong();
         }
         Map<String, String> groupingKey =
                 parseGroupingKey(config.get(METRICS_REPORTER_PROMETHEUS_PUSHGATEWAY_GROUPING_KEY));
+        boolean basicAuthEnabled = !StringUtils.isNullOrWhitespaceOnly(username);
         LOG.info(
-                "Configured PrometheusPushGatewayReporter with {hostUrl:{}, jobName:{}, randomJobNameSuffix:{}, deleteOnShutdown:{}, groupingKey:{}, pushInterval:{}}",
+                "Configured PrometheusPushGatewayReporter with {hostUrl:{}, jobName:{}, randomJobNameSuffix:{}, deleteOnShutdown:{}, groupingKey:{}, pushInterval:{}, basicAuthEnabled:{}}",
                 hostUrl,
                 jobName,
                 randomSuffix,
                 deleteOnShutdown,
                 groupingKey,
-                pushInterval);
+                pushInterval,
+                basicAuthEnabled);
         try {
             return new PrometheusPushGatewayReporter(
-                    new URL(hostUrl), jobName, groupingKey, deleteOnShutdown, pushInterval);
+                    new URL(hostUrl),
+                    jobName,
+                    groupingKey,
+                    deleteOnShutdown,
+                    pushInterval,
+                    basicAuthEnabled ? username : null,
+                    basicAuthEnabled ? password : null);
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
diff --git a/fluss-metrics/fluss-metrics-prometheus/src/test/java/org/apache/fluss/metrics/prometheus/PrometheusPushGatewayReporterTest.java b/fluss-metrics/fluss-metrics-prometheus/src/test/java/org/apache/fluss/metrics/prometheus/PrometheusPushGatewayReporterTest.java
new file mode 100644
index 000000000..a8d2f6d46
--- /dev/null
+++ b/fluss-metrics/fluss-metrics-prometheus/src/test/java/org/apache/fluss/metrics/prometheus/PrometheusPushGatewayReporterTest.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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 org.apache.fluss.metrics.prometheus;
+
+import org.apache.fluss.config.ConfigOptions;
+import org.apache.fluss.config.Configuration;
+import org.apache.fluss.config.Password;
+import org.apache.fluss.metrics.reporter.MetricReporter;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class PrometheusPushGatewayReporterTest {
+
+    private HttpServer server;
+    private BlockingQueue<String> receivedAuthHeaders;
+
+    @BeforeEach
+    void startFakePushGateway() throws IOException {
+        receivedAuthHeaders = new ArrayBlockingQueue<>(8);
+        server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
+        server.createContext(
+                "/",
+                (HttpExchange exchange) -> {
+                    // capture (possibly null) Authorization header, using empty string as absent
+                    String auth = exchange.getRequestHeaders().getFirst("Authorization");
+                    receivedAuthHeaders.offer(auth == null ? "" : auth);
+                    // drain request body so client does not block (JDK 8 compatible)
+                    try (InputStream body = exchange.getRequestBody()) {
+                        byte[] buf = new byte[1024];
+                        while (body.read(buf) != -1) {
+                            // discard
+                        }
+                    }
+
+                    exchange.sendResponseHeaders(202, -1);
+                    exchange.close();
+                });
+        server.start();
+    }
+
+    @AfterEach
+    void stopFakePushGateway() {
+        if (server != null) {
+            server.stop(0);
+        }
+    }
+
+    @Test
+    void reportSendsAuthorizationHeaderWhenBasicAuthConfigured() throws Exception {
+        PrometheusPushGatewayReporter reporter =
+                new PrometheusPushGatewayReporter(
+                        pushGatewayUrl(),
+                        "test-job",
+                        Collections.emptyMap(),
+                        false,
+                        Duration.ofSeconds(10),
+                        "myuser",
+                        "mypassword");
+        try {
+            reporter.report();
+
+            String header = receivedAuthHeaders.poll(5, TimeUnit.SECONDS);
+            assertThat(header).isNotNull().startsWith("Basic ");
+
+            String decoded =
+                    new String(
+                            Base64.getDecoder().decode(header.substring("Basic ".length())),
+                            StandardCharsets.UTF_8);
+            assertThat(decoded).isEqualTo("myuser:mypassword");
+        } finally {
+            reporter.close();
+        }
+    }
+
+    @Test
+    void reportSendsNoAuthorizationHeaderWhenBasicAuthNotConfigured() throws Exception {
+        PrometheusPushGatewayReporter reporter =
+                new PrometheusPushGatewayReporter(
+                        pushGatewayUrl(),
+                        "test-job",
+                        Collections.emptyMap(),
+                        false,
+                        Duration.ofSeconds(10),
+                        null,
+                        null);
+        try {
+            reporter.report();
+
+            String header = receivedAuthHeaders.poll(5, TimeUnit.SECONDS);
+            assertThat(header).isNotNull().isEmpty();
+        } finally {
+            reporter.close();
+        }
+    }
+
+    @Test
+    void reportSendsNoAuthorizationHeaderWhenUsernameIsBlank() throws Exception {
+        // password without username should NOT enable basic auth
+        PrometheusPushGatewayReporter reporter =
+                new PrometheusPushGatewayReporter(
+                        pushGatewayUrl(),
+                        "test-job",
+                        Collections.emptyMap(),
+                        false,
+                        Duration.ofSeconds(10),
+                        "",
+                        "somePwd");
+        try {
+            reporter.report();
+
+            String header = receivedAuthHeaders.poll(5, TimeUnit.SECONDS);
+            assertThat(header).isNotNull().isEmpty();
+        } finally {
+            reporter.close();
+        }
+    }
+
+    @Test
+    void pluginCreatesReporterCarryingBasicAuth() throws Exception {
+        Configuration config = new Configuration();
+        config.setString(
+                ConfigOptions.METRICS_REPORTER_PROMETHEUS_PUSHGATEWAY_HOST_URL,
+                pushGatewayUrl().toString());
+        config.setString(
+                ConfigOptions.METRICS_REPORTER_PROMETHEUS_PUSHGATEWAY_JOB_NAME, "plugin-job");
+        config.setString(
+                ConfigOptions.METRICS_REPORTER_PROMETHEUS_PUSHGATEWAY_USERNAME, "plugUser");
+        config.set(
+                ConfigOptions.METRICS_REPORTER_PROMETHEUS_PUSHGATEWAY_PASSWORD,
+                new Password("plugPwd"));
+        config.setString(
+                ConfigOptions.METRICS_REPORTER_PROMETHEUS_PUSHGATEWAY_GROUPING_KEY, "k1=v1");
+
+        PrometheusPushGatewayReporterPlugin plugin = new PrometheusPushGatewayReporterPlugin();
+        assertThat(plugin.identifier()).isEqualTo("prometheus-push");
+
+        MetricReporter reporter = plugin.createMetricReporter(config);
+        assertThat(reporter).isInstanceOf(PrometheusPushGatewayReporter.class);
+        try {
+            ((PrometheusPushGatewayReporter) reporter).report();
+
+            String header = receivedAuthHeaders.poll(5, TimeUnit.SECONDS);
+            assertThat(header).isNotNull().startsWith("Basic ");
+            String decoded =
+                    new String(
+                            Base64.getDecoder().decode(header.substring("Basic ".length())),
+                            StandardCharsets.UTF_8);
+            assertThat(decoded).isEqualTo("plugUser:plugPwd");
+        } finally {
+            reporter.close();
+        }
+    }
+
+    private URL pushGatewayUrl() throws IOException {
+        return new URL("http://127.0.0.1:" + server.getAddress().getPort());
+    }
+}
diff --git a/website/docs/maintenance/configuration.md b/website/docs/maintenance/configuration.md
index 0a4223074..39e38a4c8 100644
--- a/website/docs/maintenance/configuration.md
+++ b/website/docs/maintenance/configuration.md
@@ -198,6 +198,8 @@ More metrics example could be found in [Observability - Metric Reporters](observ
 | metrics.reporter.prometheus-push.random-job-name-suffix | Boolean  | true       | Specifies whether a random suffix should be appended to the job name, defaults to true. This is useful when multiple instances of the reporter are running on the same host.                                                                                                                             |
 | metrics.reporter.prometheus-push.delete-on-shutdown     | Boolean  | true       | Specifies whether to delete metrics from the PushGateway on shutdown, defaults to true. Fluss will try its best to delete the metrics but this is not guaranteed.                                                                                                                                        | 
 | metrics.reporter.prometheus-push.grouping-key           | String   | (None)     | Specifies the grouping key which is the group and global labels of all metrics. The label name and value are separated by '=', and labels are separated by ';', e.g., k1=v1;k2=v2.                                                                                                                       | 
+| metrics.reporter.prometheus-push.username               | String   | (None)     | The username for Basic Auth of the Prometheus PushGateway. Leave it unset to disable authentication.                                                                                                                                                                                                | 
+| metrics.reporter.prometheus-push.password               | String   | (None)     | The password for Basic Auth of the Prometheus PushGateway. Only takes effect when username is configured.                                                                                                                                                                                           | 
 ## Lakehouse
 
 | Option           | Type    | Default | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
diff --git a/website/docs/maintenance/observability/metric-reporters.md b/website/docs/maintenance/observability/metric-reporters.md
index 4afdca5b4..bea71c010 100644
--- a/website/docs/maintenance/observability/metric-reporters.md
+++ b/website/docs/maintenance/observability/metric-reporters.md
@@ -93,6 +93,8 @@ Parameters:
 - `metrics.reporter.prometheus-push.random-job-name-suffix` - (Optional) Specifies whether a random suffix should be appended to the job name, defaults to true. This is useful when multiple instances of the reporter are running on the same host. 
 - `metrics.reporter.prometheus-push.delete-on-shutdown` - (Optional) Specifies whether to delete metrics from the PushGateway on shutdown, defaults to true. Fluss will try its best to delete the metrics but this is not guaranteed.
 - `metrics.reporter.prometheus-push.grouping-key` - Specifies the grouping key which is the group and global labels of all metrics. The label name and value are separated by `=`, and labels are separated by `;`, e.g., `k1=v1;k2=v2`.
+- `metrics.reporter.prometheus-push.username` - (Optional) The username for Basic Auth of the Prometheus PushGateway. Leave it unset to disable authentication.
+- `metrics.reporter.prometheus-push.password` - (Optional) The password for Basic Auth of the Prometheus PushGateway. Only takes effect when `username` is configured.
 
 Example configuration:
 
@@ -104,6 +106,8 @@ metrics.reporter.prometheus-push.push-interval: 10 SECONDS
 metrics.reporter.prometheus-push.random-job-name-suffix: true
 metrics.reporter.prometheus-push.delete-on-shutdown: true
 metrics.reporter.prometheus-push.grouping-key: instance=instance01;cluster=clusterA
+metrics.reporter.prometheus-push.username: myuser
+metrics.reporter.prometheus-push.password: mypassword
 ```
 
 ### InfluxDB