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秒。 | | 绑定标签 | 用于对监控资源进行分类管理。 |