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 <[email protected]>
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

Reply via email to