From c1db97ecade42a0241f1181acbe15b0e9ca63e9b Mon Sep 17 00:00:00 2001 From: NekoPunch Date: Thu, 9 Jul 2026 00:14:24 -0700 Subject: [PATCH] [fix] expose prestodb auth parameters and wire authorization into all metrics (#2838) The prestodb template's http sections referenced authType/username/password placeholders in authorization blocks, but the params were never defined, so the UI offered no way to enter credentials. Define the missing params, and add the authorization block to the cluster (priority 0 availability) and node metrics which previously sent unauthenticated requests, causing auth-enabled PrestoDB deployments to be marked down. Update en/zh-cn help docs and add an e2e test that collects all metric groups through a mock server enforcing HTTP Basic Auth. --- .../basic/http/PrestodbMonitorE2eTest.java | 183 ++++++++++++++++++ .../main/resources/define/app-prestodb.yml | 42 ++++ home/docs/help/prestodb.md | 4 + .../current/help/prestodb.md | 4 + 4 files changed, 233 insertions(+) create mode 100644 hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/http/PrestodbMonitorE2eTest.java diff --git a/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/http/PrestodbMonitorE2eTest.java b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/http/PrestodbMonitorE2eTest.java new file mode 100644 index 00000000000..04a821c0cce --- /dev/null +++ b/hertzbeat-e2e/hertzbeat-collector-basic-e2e/src/test/java/org/apache/hertzbeat/collector/collect/basic/http/PrestodbMonitorE2eTest.java @@ -0,0 +1,183 @@ +/* + * 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.hertzbeat.collector.collect.basic.http; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollectE2eTest; +import org.apache.hertzbeat.collector.collect.http.HttpCollectImpl; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.entity.job.Configmap; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.Protocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.test.context.SpringBootTest; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +/** + * E2E test for PrestoDB monitor with HTTP Basic Auth enabled (issue #2838). + * Unlike other http monitor tests, the protocol is taken from the template definition + * after placeholder replacement (mirroring WheelTimerTask#initJobMetrics), so a metric + * whose http section lacks the authorization wiring fails this test with a 401. + */ +@Slf4j +@ExtendWith(MockitoExtension.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class PrestodbMonitorE2eTest extends AbstractCollectE2eTest { + + private static final int MOCK_SERVER_PORT = 52390; + private static final String LOCALHOST = "127.0.0.1"; + private static final String USERNAME = "presto-admin"; + private static final String PASSWORD = "presto-secret"; + private static final Gson GSON = new Gson(); + private static HttpServer mockServer; + + private static final String CLUSTER_JSON = """ + {"activeWorkers": 3, "runningQueries": 2, "queuedQueries": 1, + "blockedQueries": 0, "runningDrivers": 12, "runningTasks": 5}"""; + + private static final String NODE_JSON = """ + [{"uri": "http://127.0.0.1:8080", "recentRequests": 25.3, "recentFailures": 0.0, + "recentSuccesses": 25.3, "lastRequestTime": "2026-07-08T10:00:00.000Z", + "lastResponseTime": "2026-07-08T10:00:00.100Z", "age": "5.20d", "recentFailureRatio": 0.0}]"""; + + private static final String STATUS_JSON = """ + {"nodeId": "coordinator-1", "nodeVersion": {"version": "0.289"}, "environment": "production", + "coordinator": true, "uptime": "5.20d", "externalAddress": "127.0.0.1", + "internalAddress": "127.0.0.1", "processors": 8, "processCpuLoad": 0.35, + "systemCpuLoad": 0.42, "heapUsed": 1073741824, "heapAvailable": 4294967296, "nonHeapUsed": 268435456}"""; + + private static final String TASK_JSON = """ + [{"taskId": "20260708_100000_00001_abcde.1.0.0", "version": 42, "state": "RUNNING", + "self": "http://127.0.0.1:8080/v1/task/20260708_100000_00001_abcde.1.0.0", + "lastHeartbeat": "2026-07-08T10:00:00.000Z"}]"""; + + @AfterEach + public void tearDown() { + if (mockServer != null) { + mockServer.stop(0); + } + } + + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + collect = new HttpCollectImpl(); + + mockServer = HttpServer.create(new InetSocketAddress(MOCK_SERVER_PORT), 0); + mockServer.setExecutor(null); + mockServer.start(); + mockServer.createContext("/v1/cluster", exchange -> sendAuthenticatedJson(exchange, CLUSTER_JSON)); + mockServer.createContext("/v1/node", exchange -> sendAuthenticatedJson(exchange, NODE_JSON)); + mockServer.createContext("/v1/status", exchange -> sendAuthenticatedJson(exchange, STATUS_JSON)); + mockServer.createContext("/v1/task", exchange -> sendAuthenticatedJson(exchange, TASK_JSON)); + } + + private void sendAuthenticatedJson(HttpExchange exchange, String response) throws IOException { + String expected = "Basic " + Base64.getEncoder() + .encodeToString((USERNAME + ":" + PASSWORD).getBytes(StandardCharsets.UTF_8)); + if (!expected.equals(exchange.getRequestHeaders().getFirst("Authorization"))) { + exchange.getResponseHeaders().set("WWW-Authenticate", "Basic realm=\"presto\""); + exchange.sendResponseHeaders(401, -1); + exchange.close(); + return; + } + exchange.getResponseHeaders().set("Content-Type", "application/json"); + final byte[] array = response.getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, array.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(array); + } + } + + @Test + public void testPrestodbMonitorWithBasicAuth() { + Job prestodbJob = appService.getAppDefine("prestodb"); + Map configmap = buildParamConfigmap(true); + for (Metrics metricsDef : prestodbJob.getMetrics()) { + metricsDef = replaceJobPlaceholder(metricsDef, configmap); + validateMetricsCollection(metricsDef, metricsDef.getName()); + } + } + + @Test + public void testPrestodbMonitorWithoutAuthIsRejected() { + Job prestodbJob = appService.getAppDefine("prestodb"); + Map configmap = buildParamConfigmap(false); + Metrics availabilityMetric = prestodbJob.getMetrics().stream() + .filter(m -> "cluster".equals(m.getName())) + .findFirst() + .orElseThrow(() -> new IllegalStateException("prestodb template has no cluster metric")); + availabilityMetric = replaceJobPlaceholder(availabilityMetric, configmap); + CollectRep.MetricsData.Builder metricsData = collectMetrics(availabilityMetric); + Assertions.assertNotEquals(CollectRep.Code.SUCCESS, metricsData.getCode(), + "collection against an auth-protected endpoint must fail when no credentials are configured"); + } + + private Map buildParamConfigmap(boolean withAuth) { + Map configmap = new HashMap<>(); + configmap.put("host", new Configmap("host", LOCALHOST, (byte) 1)); + configmap.put("port", new Configmap("port", String.valueOf(MOCK_SERVER_PORT), (byte) 1)); + configmap.put("ssl", new Configmap("ssl", "false", (byte) 1)); + configmap.put("timeout", new Configmap("timeout", "6000", (byte) 1)); + if (withAuth) { + configmap.put("authType", new Configmap("authType", "Basic Auth", (byte) 1)); + configmap.put("username", new Configmap("username", USERNAME, (byte) 1)); + configmap.put("password", new Configmap("password", PASSWORD, (byte) 1)); + } + return configmap; + } + + /** + * Mirrors WheelTimerTask#initJobMetrics: replace ^_^param^_^ placeholders + * in the whole metric definition, keeping the template's http authorization wiring. + */ + private Metrics replaceJobPlaceholder(Metrics metricsDef, Map configmap) { + JsonElement jsonElement = GSON.toJsonTree(metricsDef); + CollectUtil.replaceSmilingPlaceholder(jsonElement, configmap); + return GSON.fromJson(jsonElement, Metrics.class); + } + + @Override + protected Protocol buildProtocol(Metrics metricsDef) { + return metricsDef.getHttp(); + } + + @Override + protected CollectRep.MetricsData.Builder collectMetrics(Metrics metricsDef) { + metrics.setHttp(metricsDef.getHttp()); + return collectMetricsData(metrics, metricsDef); + } +} diff --git a/hertzbeat-manager/src/main/resources/define/app-prestodb.yml b/hertzbeat-manager/src/main/resources/define/app-prestodb.yml index 120e92afb0c..813681e009c 100644 --- a/hertzbeat-manager/src/main/resources/define/app-prestodb.yml +++ b/hertzbeat-manager/src/main/resources/define/app-prestodb.yml @@ -53,6 +53,36 @@ params: ja-JP: HTTPS type: boolean required: true + - field: authType + name: + zh-CN: 认证方式 + en-US: Auth Type + ja-JP: 認証方法 + type: radio + required: false + hide: true + options: + - label: Basic Auth + value: Basic Auth + - label: Digest Auth + value: Digest Auth + - field: username + name: + zh-CN: 用户名 + en-US: Username + ja-JP: ユーザー名 + type: text + limit: 50 + required: false + hide: true + - field: password + name: + zh-CN: 密码 + en-US: Password + ja-JP: パスワード + type: password + required: false + hide: true - field: timeout name: zh-CN: 超时时间(ms) @@ -115,6 +145,12 @@ metrics: timeout: ^_^timeout^_^ method: GET ssl: ^_^ssl^_^ + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ parseType: jsonPath parseScript: '$' @@ -182,6 +218,12 @@ metrics: timeout: ^_^timeout^_^ method: GET ssl: ^_^ssl^_^ + authorization: + type: ^_^authType^_^ + basicAuthUsername: ^_^username^_^ + basicAuthPassword: ^_^password^_^ + digestAuthUsername: ^_^username^_^ + digestAuthPassword: ^_^password^_^ parseType: jsonPath parseScript: '$[*]' diff --git a/home/docs/help/prestodb.md b/home/docs/help/prestodb.md index 2ebe19fcf87..58081d1ded8 100644 --- a/home/docs/help/prestodb.md +++ b/home/docs/help/prestodb.md @@ -13,6 +13,10 @@ keywords: [ open source monitoring system, open source database monitoring, pres |---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| | Target Host | The IP address, IPv4, IPv6, or domain name of the target to be monitored. Note: ⚠️ Do not include protocol headers (e.g., https://, http://). | | port | Port | +| HTTPS | Whether to enable HTTPS for the PrestoDB API endpoint. | +| Auth Type | Optional HTTP authentication mode. Supported values are `Basic Auth` and `Digest Auth`. | +| Username | Username used when `Basic Auth` or `Digest Auth` is enabled. | +| Password | Password used when `Basic Auth` or `Digest Auth` is enabled. | | Task Name | The name identifying this monitor, which must be unique. | | Connection Timeout | Timeout for PrestoDB connection when no response is received, in milliseconds (ms). Default is 6000 ms. | | Collection Interval | Interval for periodic data collection, in seconds. The minimum interval is 30 seconds. | diff --git a/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/help/prestodb.md b/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/help/prestodb.md index 423f35f49f2..a2216cdcbdd 100644 --- a/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/help/prestodb.md +++ b/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/help/prestodb.md @@ -14,6 +14,10 @@ keywords: [ 开源监控系统, 开源数据库监控, Presto数据库监控 ] | 目标Host | 被监控的对端IPV4,IPV6或域名。注意⚠️不带协议头(eg: https://, http://)。 | | 任务名称 | 标识此监控的名称,名称需要保证唯一性。 | | 端口 | 被监控的平台端口。 | +| 启用HTTPS | 是否启用 PrestoDB API 的 HTTPS 访问。 | +| 认证方式 | 可选的 HTTP 认证方式,支持 `Basic Auth` 和 `Digest Auth`。 | +| 用户名 | 启用 `Basic Auth` 或 `Digest Auth` 后使用的用户名。 | +| 密码 | 启用 `Basic Auth` 或 `Digest Auth` 后使用的密码。 | | 连接超时时间 | 设置连接PrestoDB未响应数据时的超时时间,单位ms毫秒,默认6000毫秒。 | | 采集间隔 | 监控周期性采集数据间隔时间,单位秒,可设置的最小间隔为30秒。 | | 绑定标签 | 用于对监控资源进行分类管理。 |