Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<String, Configmap> 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<String, Configmap> 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<String, Configmap> buildParamConfigmap(boolean withAuth) {
Map<String, Configmap> 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<String, Configmap> 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);
}
}
42 changes: 42 additions & 0 deletions hertzbeat-manager/src/main/resources/define/app-prestodb.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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: '$'

Expand Down Expand Up @@ -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: '$[*]'

Expand Down
4 changes: 4 additions & 0 deletions home/docs/help/prestodb.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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秒。 |
| 绑定标签 | 用于对监控资源进行分类管理。 |
Expand Down
Loading