mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 18:19:02 +00:00
Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09103ae89c | ||
|
|
5cdfe545b5 | ||
|
+16 |
9be7295084 | ||
|
|
6905bcc986 | ||
|
|
1050032a13 | ||
|
|
9b223dd217 | ||
|
|
0a1b5961e9 | ||
|
|
a8a70b76d7 | ||
|
+1 |
05b63903b5 | ||
|
|
7d8b55dfb8 | ||
|
|
72a3e59ca6 | ||
|
|
3ab94a751b | ||
|
|
66049f189b | ||
|
|
69ec3f8751 | ||
|
|
feb43c5678 | ||
|
|
1fae67282d | ||
|
|
886f0b74f6 | ||
|
|
32e1a43aa5 | ||
|
|
dcd806802e | ||
|
|
5746cbbbd7 | ||
|
|
e23903fd0c | ||
|
|
780710f226 | ||
|
|
4607ede541 |
+82
-4
@@ -1270,10 +1270,10 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "a-little-fool",
|
||||
"name": "小笨蛋",
|
||||
"login": "lynx009",
|
||||
"name": "lynx009",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/105542329?v=4",
|
||||
"profile": "https://github.com/a-little-fool",
|
||||
"profile": "https://github.com/lynx009",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc",
|
||||
@@ -1442,7 +1442,7 @@
|
||||
},
|
||||
{
|
||||
"login": "Calvin979",
|
||||
"name": "Naruse",
|
||||
"name": "Calvin",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/131688897?v=4",
|
||||
"profile": "https://github.com/Calvin979",
|
||||
"contributions": [
|
||||
@@ -2523,6 +2523,84 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "yy549159265",
|
||||
"name": "yy549159265",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/40821310?v=4",
|
||||
"profile": "https://github.com/yy549159265",
|
||||
"contributions": [
|
||||
"code",
|
||||
"test",
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "cto-huhang",
|
||||
"name": "cto-huhang",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/53338629?v=4",
|
||||
"profile": "https://github.com/cto-huhang",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Saramanda9988",
|
||||
"name": "LunaRain_079",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/176664901?v=4",
|
||||
"profile": "https://github.com/Saramanda9988",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "delei",
|
||||
"name": "DeleiGuo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/17263766?v=4",
|
||||
"profile": "https://github.com/delei",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"code",
|
||||
"test",
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "chingjustwe",
|
||||
"name": "Rocky, Chi",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/13643747?v=4",
|
||||
"profile": "https://github.com/chingjustwe",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "rowankid",
|
||||
"name": "Wenqi Luo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/18652781?v=4",
|
||||
"profile": "https://github.com/rowankid",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "tuzuy",
|
||||
"name": "tuzuy",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/95274591?v=4",
|
||||
"profile": "https://github.com/tuzuy",
|
||||
"contributions": [
|
||||
"code",
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "carlpinto25",
|
||||
"name": "carl pinto",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/117299909?v=4",
|
||||
"profile": "https://github.com/carlpinto25",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -572,8 +572,8 @@ HertzBeat is built on so many great open source projects, thanks to them!
|
||||
<p align="left">
|
||||
<img src="./home/static/img/home/cncf-landscape-left-logo.svg" width="300"> <img src="./home/static/img/home/cncf-right-logo.svg" width="345" />
|
||||
<br /><br />
|
||||
HertzBeat has been included in the <a href="https://landscape.cncf.io/guide?item=observability-and-analysis--monitoring--hertzbeat#observability-and-analysis--monitoring">
|
||||
CNCF Observability And Analysis - Monitoring Landscape.</a>
|
||||
HertzBeat has been included in the <a href="https://landscape.cncf.io/?item=observability-and-analysis--observability--hertzbeat">
|
||||
CNCF Observability And Analysis - Observability Landscape.</a>
|
||||
</p>
|
||||
|
||||
## 🛡️ License
|
||||
|
||||
+2
-2
@@ -572,8 +572,8 @@ HertzBeat is built on so many great open source projects, thanks to them!
|
||||
<p align="left">
|
||||
<img src="./home/static/img/home/cncf-landscape-left-logo.svg" width="300"> <img src="./home/static/img/home/cncf-right-logo.svg" width="345" />
|
||||
<br /><br />
|
||||
HertzBeat has been included in the <a href="https://landscape.cncf.io/guide?item=observability-and-analysis--monitoring--hertzbeat#observability-and-analysis--monitoring">
|
||||
CNCF Observability And Analysis - Monitoring Landscape.</a>
|
||||
HertzBeat has been included in the <a href="https://landscape.cncf.io/?item=observability-and-analysis--observability--hertzbeat">
|
||||
CNCF Observability And Analysis - Observability Landscape.</a>
|
||||
</p>
|
||||
|
||||
## 🛡️ License
|
||||
|
||||
+2
-2
@@ -576,8 +576,8 @@ HertzBeat is built on so many great open source projects, thanks to them!
|
||||
<p align="left">
|
||||
<img src="./home/static/img/home/cncf-landscape-left-logo.svg" width="300"> <img src="./home/static/img/home/cncf-right-logo.svg" width="345" />
|
||||
<br /><br />
|
||||
HertzBeat has been included in the <a href="https://landscape.cncf.io/guide?item=observability-and-analysis--monitoring--hertzbeat#observability-and-analysis--monitoring">
|
||||
CNCF Observability And Analysis - Monitoring Landscape.</a>
|
||||
HertzBeat has been included in the <a href="https://landscape.cncf.io/?item=observability-and-analysis--observability--hertzbeat">
|
||||
CNCF Observability And Analysis - Observability Landscape.</a>
|
||||
</p>
|
||||
|
||||
## 🛡️ License
|
||||
|
||||
@@ -35,6 +35,10 @@
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
@@ -53,6 +57,10 @@
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-alerter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.usthe.sureness</groupId>
|
||||
<artifactId>spring-boot3-starter-sureness</artifactId>
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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.ai.agent.adapters;
|
||||
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.Hierarchy;
|
||||
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||
import org.springframework.data.domain.Page;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface that provides access to alert definition information by retrieving data
|
||||
* through the underlying alert define service.
|
||||
*/
|
||||
public interface AlertDefineServiceAdapter {
|
||||
|
||||
/**
|
||||
* Add a new alert rule definition
|
||||
* @param alertDefine Alert definition to add
|
||||
* @return Created alert definition with ID
|
||||
*/
|
||||
AlertDefine addAlertDefine(AlertDefine alertDefine);
|
||||
|
||||
/**
|
||||
* Get alert definitions with filtering and pagination
|
||||
* @param search Search term
|
||||
* @param app Monitor type filter
|
||||
* @param enabled Enabled status filter
|
||||
* @param sort Sort field
|
||||
* @param order Sort order
|
||||
* @param pageIndex Page index
|
||||
* @param pageSize Page size
|
||||
* @return Page of alert definitions
|
||||
*/
|
||||
Page<AlertDefine> getAlertDefines(String search, String app, Boolean enabled, String sort, String order, int pageIndex, int pageSize);
|
||||
|
||||
/**
|
||||
* Get alert definition by ID
|
||||
* @param id Alert definition ID
|
||||
* @return Alert definition if found
|
||||
*/
|
||||
AlertDefine getAlertDefine(Long id);
|
||||
|
||||
/**
|
||||
* Enable or disable alert definition
|
||||
* @param id Alert definition ID
|
||||
* @param enabled Whether to enable
|
||||
*/
|
||||
void toggleAlertDefineStatus(Long id, boolean enabled);
|
||||
|
||||
/**
|
||||
* Modify/update an existing alert definition
|
||||
* @param alertDefine Alert definition to update
|
||||
* @return Updated alert definition
|
||||
*/
|
||||
AlertDefine modifyAlertDefine(AlertDefine alertDefine);
|
||||
|
||||
/**
|
||||
* Get specific app hierarchy structure
|
||||
* @param app App type
|
||||
* @param lang Language for localization
|
||||
* @return List of hierarchy objects for specific app
|
||||
*/
|
||||
|
||||
List<Hierarchy> getAppHierarchy(String app, String lang);
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.ai.agent.adapters;
|
||||
|
||||
import org.apache.hertzbeat.alert.dto.AlertSummary;
|
||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||
import org.springframework.data.domain.Page;
|
||||
|
||||
/**
|
||||
* Interface that provides access to alert information by retrieving alert data
|
||||
* through the underlying alert service.
|
||||
*/
|
||||
public interface AlertServiceAdapter {
|
||||
|
||||
/**
|
||||
* Get single alerts with filtering and pagination
|
||||
* @param status Alert status
|
||||
* @param search Search term
|
||||
* @param sort Sort field
|
||||
* @param order Sort order
|
||||
* @param pageIndex Page index
|
||||
* @param pageSize Page size
|
||||
* @return Page of single alerts
|
||||
*/
|
||||
Page<SingleAlert> getSingleAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize);
|
||||
|
||||
/**
|
||||
* Get group alerts with filtering and pagination
|
||||
* @param status Alert status
|
||||
* @param search Search term
|
||||
* @param sort Sort field
|
||||
* @param order Sort order
|
||||
* @param pageIndex Page index
|
||||
* @param pageSize Page size
|
||||
* @return Page of group alerts
|
||||
*/
|
||||
Page<GroupAlert> getGroupAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize);
|
||||
|
||||
/**
|
||||
* Get alerts summary statistics
|
||||
* @return Alert summary information
|
||||
*/
|
||||
AlertSummary getAlertsSummary();
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.ai.agent.adapters;
|
||||
|
||||
import org.apache.hertzbeat.common.entity.dto.MetricsData;
|
||||
import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData;
|
||||
|
||||
/**
|
||||
* Interface that provides access to metrics information by retrieving metrics data
|
||||
* through the underlying metrics service.
|
||||
*/
|
||||
public interface MetricsServiceAdapter {
|
||||
|
||||
/**
|
||||
* Check warehouse storage server status
|
||||
* @return true if warehouse is available, false otherwise
|
||||
*/
|
||||
Boolean getWarehouseStorageServerStatus();
|
||||
|
||||
/**
|
||||
* Query real-time metrics data
|
||||
* @param monitorId Monitor ID
|
||||
* @param metrics Metrics name
|
||||
* @return Real-time metrics data
|
||||
*/
|
||||
MetricsData getMetricsData(Long monitorId, String metrics);
|
||||
|
||||
/**
|
||||
* Query historical metrics data
|
||||
* @param monitorId Monitor ID
|
||||
* @param app Monitor type
|
||||
* @param metrics Metrics name
|
||||
* @param metric Metric field name
|
||||
* @param label Label filter
|
||||
* @param history Query historical time period
|
||||
* @param interval Whether to aggregate data
|
||||
* @return Historical metrics data
|
||||
*/
|
||||
MetricsHistoryData getMetricHistoryData(Long monitorId, String app, String metrics, String metric, String label, String history, Boolean interval);
|
||||
}
|
||||
+40
-10
@@ -20,7 +20,10 @@ package org.apache.hertzbeat.ai.agent.adapters;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.apache.hertzbeat.common.entity.manager.Monitor;
|
||||
import org.apache.hertzbeat.common.entity.manager.Param;
|
||||
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Interface that provides access to monitor information by retrieving monitor data
|
||||
@@ -28,14 +31,41 @@ import java.util.List;
|
||||
*/
|
||||
public interface MonitorServiceAdapter {
|
||||
Page<Monitor> getMonitors(
|
||||
List<Long> ids,
|
||||
String app,
|
||||
String search,
|
||||
Byte status,
|
||||
String sort,
|
||||
String order,
|
||||
Integer pageIndex,
|
||||
Integer pageSize,
|
||||
String labels
|
||||
List<Long> ids,
|
||||
String app,
|
||||
String search,
|
||||
Byte status,
|
||||
String sort,
|
||||
String order,
|
||||
Integer pageIndex,
|
||||
Integer pageSize,
|
||||
String labels
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new monitor
|
||||
*
|
||||
* @param monitor The monitor entity to create
|
||||
* @param params List of parameters for the monitor
|
||||
* @param collector Optional collector assignment
|
||||
* @return The created monitor ID
|
||||
*/
|
||||
Long addMonitor(Monitor monitor, List<Param> params, String collector);
|
||||
|
||||
/**
|
||||
* Get all available monitor types with their display names
|
||||
*
|
||||
* @param language Language code (e.g., "en-US", "zh-CN")
|
||||
* @return Map of monitor type key to display name
|
||||
*/
|
||||
Map<String, String> getAvailableMonitorTypes(String language);
|
||||
|
||||
/**
|
||||
* Get parameter definitions for a specific monitor type
|
||||
*
|
||||
* @param app Monitor type/application name (e.g., "linux", "mysql", "redis")
|
||||
* @return List of parameter definitions for the monitor type
|
||||
*/
|
||||
List<ParamDefine> getMonitorParamDefines(String app);
|
||||
}
|
||||
|
||||
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* 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.ai.agent.adapters.impl;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.adapters.AlertDefineServiceAdapter;
|
||||
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.Hierarchy;
|
||||
import org.apache.hertzbeat.ai.agent.utils.UtilityClass;
|
||||
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||
import org.apache.hertzbeat.common.support.SpringContextHolder;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Implementation of the AlertDefineServiceAdapter interface that provides access to alert definition information
|
||||
* through reflection by invoking the underlying alert define service implementation.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class AlertDefineServiceAdapterImpl implements AlertDefineServiceAdapter {
|
||||
|
||||
@Override
|
||||
public AlertDefine addAlertDefine(AlertDefine alertDefine) {
|
||||
try {
|
||||
Object alertDefineService = null;
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current security subject for addAlertDefine: {}", subjectSum);
|
||||
|
||||
try {
|
||||
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
|
||||
}
|
||||
|
||||
assert alertDefineService != null;
|
||||
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
|
||||
|
||||
Method method = alertDefineService.getClass().getMethod("addAlertDefine", AlertDefine.class);
|
||||
|
||||
method.invoke(alertDefineService, alertDefine);
|
||||
|
||||
log.debug("Successfully added alert define with ID: {}", alertDefine.getId());
|
||||
return alertDefine;
|
||||
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException("Method not found: addAlertDefine", e);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to invoke addAlertDefine via adapter", e);
|
||||
throw new RuntimeException("Failed to invoke addAlertDefine via adapter: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<AlertDefine> getAlertDefines(String search, String app, Boolean enabled, String sort, String order, int pageIndex, int pageSize) {
|
||||
try {
|
||||
Object alertDefineService = null;
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current security subject for getAlertDefines: {}", subjectSum);
|
||||
|
||||
try {
|
||||
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
|
||||
}
|
||||
|
||||
assert alertDefineService != null;
|
||||
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
|
||||
|
||||
Method method = alertDefineService.getClass().getMethod(
|
||||
"getAlertDefines",
|
||||
List.class, String.class, String.class, String.class, int.class, int.class);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Page<AlertDefine> result = (Page<AlertDefine>) method.invoke(
|
||||
alertDefineService, null, search, sort, order, pageIndex, pageSize);
|
||||
|
||||
log.debug("Successfully retrieved {} alert defines", result.getContent().size());
|
||||
return result;
|
||||
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException("Method not found: getAlertDefines", e);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to invoke getAlertDefines via adapter", e);
|
||||
throw new RuntimeException("Failed to invoke getAlertDefines via adapter: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AlertDefine getAlertDefine(Long id) {
|
||||
try {
|
||||
Object alertDefineService = null;
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current security subject for getAlertDefine: {}", subjectSum);
|
||||
|
||||
try {
|
||||
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
|
||||
}
|
||||
|
||||
assert alertDefineService != null;
|
||||
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
|
||||
|
||||
Method method = alertDefineService.getClass().getMethod("getAlertDefine", long.class);
|
||||
|
||||
AlertDefine result = (AlertDefine) method.invoke(alertDefineService, id);
|
||||
|
||||
log.debug("Successfully retrieved alert define with ID: {}", id);
|
||||
return result;
|
||||
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException("Method not found: getAlertDefine", e);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to invoke getAlertDefine via adapter for ID: {}", id, e);
|
||||
throw new RuntimeException("Failed to invoke getAlertDefine via adapter: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void toggleAlertDefineStatus(Long id, boolean enabled) {
|
||||
try {
|
||||
Object alertDefineService = null;
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current security subject for toggleAlertDefineStatus: {}", subjectSum);
|
||||
|
||||
try {
|
||||
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
|
||||
}
|
||||
|
||||
assert alertDefineService != null;
|
||||
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
|
||||
|
||||
// First get the existing AlertDefine
|
||||
Method getMethod = alertDefineService.getClass().getMethod("getAlertDefine", long.class);
|
||||
AlertDefine alertDefine = (AlertDefine) getMethod.invoke(alertDefineService, id);
|
||||
|
||||
if (alertDefine == null) {
|
||||
throw new RuntimeException("AlertDefine with ID " + id + " not found");
|
||||
}
|
||||
|
||||
// Update the enable status
|
||||
alertDefine.setEnable(enabled);
|
||||
|
||||
// Use modifyAlertDefine to save the changes
|
||||
Method modifyMethod = alertDefineService.getClass().getMethod("modifyAlertDefine", AlertDefine.class);
|
||||
modifyMethod.invoke(alertDefineService, alertDefine);
|
||||
|
||||
log.debug("Successfully toggled alert define status for ID: {} to enabled: {}", id, enabled);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to invoke toggleAlertDefineStatus via adapter for ID: {}", id, e);
|
||||
throw new RuntimeException("Failed to invoke toggleAlertDefineStatus via adapter: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AlertDefine modifyAlertDefine(AlertDefine alertDefine) {
|
||||
try {
|
||||
Object alertDefineService = null;
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current security subject for modifyAlertDefine: {}", subjectSum);
|
||||
|
||||
try {
|
||||
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
|
||||
}
|
||||
|
||||
assert alertDefineService != null;
|
||||
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
|
||||
|
||||
Method method = alertDefineService.getClass().getMethod("modifyAlertDefine", AlertDefine.class);
|
||||
|
||||
method.invoke(alertDefineService, alertDefine);
|
||||
|
||||
log.debug("Successfully modified alert define with ID: {}", alertDefine.getId());
|
||||
return alertDefine;
|
||||
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException("Method not found: modifyAlertDefine", e);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to invoke modifyAlertDefine via adapter", e);
|
||||
throw new RuntimeException("Failed to invoke modifyAlertDefine via adapter: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the application hierarchy for a given app and language.
|
||||
* Uses reflection to call the underlying app service method.
|
||||
*
|
||||
* @param app The application name
|
||||
* @param lang The language code (optional, defaults to "en-US")
|
||||
* @return List of Hierarchy objects representing the app hierarchy
|
||||
*/
|
||||
|
||||
@Override
|
||||
public List<Hierarchy> getAppHierarchy(String app, String lang) {
|
||||
try {
|
||||
Object appService = null;
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current security subject for getAppHierarchy: {}", subjectSum);
|
||||
|
||||
try {
|
||||
appService = SpringContextHolder.getBean("appServiceImpl");
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not find bean by name 'appServiceImpl', trying by class name");
|
||||
}
|
||||
|
||||
assert appService != null;
|
||||
log.debug("AppService bean found for getAppHierarchy: {}", appService.getClass().getSimpleName());
|
||||
|
||||
// Provide default language if not specified
|
||||
if (lang == null || lang.trim().isEmpty()) {
|
||||
lang = "en-US";
|
||||
}
|
||||
|
||||
// Call getAppHierarchy method: getAppHierarchy(String app, String lang)
|
||||
Method method = appService.getClass().getMethod("getAppHierarchy", String.class, String.class);
|
||||
|
||||
List<?> managerHierarchies = (List<?>) method.invoke(appService, app, lang);
|
||||
|
||||
// Convert manager DTOs to ai-agent DTOs
|
||||
List<Hierarchy> result = UtilityClass.convertToAgentHierarchies(managerHierarchies);
|
||||
|
||||
log.debug("Successfully retrieved and converted {} hierarchies for app '{}'", result.size(), app);
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get app hierarchy for app '{}': {}", app, e.getMessage(), e);
|
||||
throw new RuntimeException("Failed to get app hierarchy for " + app, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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.ai.agent.adapters.impl;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.adapters.AlertServiceAdapter;
|
||||
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
|
||||
import org.apache.hertzbeat.alert.dto.AlertSummary;
|
||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||
import org.apache.hertzbeat.common.support.SpringContextHolder;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Implementation of the AlertServiceAdapter interface that provides access to alert information
|
||||
* through reflection by invoking the underlying alert service implementation.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class AlertServiceAdapterImpl implements AlertServiceAdapter {
|
||||
|
||||
@Override
|
||||
public Page<SingleAlert> getSingleAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize) {
|
||||
try {
|
||||
Object alertService = null;
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current security subject for getSingleAlerts: {}", subjectSum);
|
||||
|
||||
try {
|
||||
alertService = SpringContextHolder.getBean("alertServiceImpl");
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not find bean by name 'alertServiceImpl'");
|
||||
}
|
||||
|
||||
assert alertService != null;
|
||||
log.debug("AlertService bean found: {}", alertService.getClass().getSimpleName());
|
||||
|
||||
Method method = alertService.getClass().getMethod(
|
||||
"getSingleAlerts",
|
||||
String.class, String.class, String.class, String.class, int.class, int.class);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Page<SingleAlert> result = (Page<SingleAlert>) method.invoke(
|
||||
alertService, status, search, sort, order, pageIndex, pageSize);
|
||||
|
||||
log.debug("Successfully retrieved {} single alerts", result.getContent().size());
|
||||
return result;
|
||||
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException("Method not found: getSingleAlerts", e);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to invoke getSingleAlerts via adapter", e);
|
||||
throw new RuntimeException("Failed to invoke getSingleAlerts via adapter: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<GroupAlert> getGroupAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize) {
|
||||
try {
|
||||
Object alertService = null;
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current security subject for getGroupAlerts: {}", subjectSum);
|
||||
|
||||
try {
|
||||
alertService = SpringContextHolder.getBean("alertServiceImpl");
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not find bean by name 'alertServiceImpl'");
|
||||
}
|
||||
|
||||
assert alertService != null;
|
||||
log.debug("AlertService bean found: {}", alertService.getClass().getSimpleName());
|
||||
|
||||
Method method = alertService.getClass().getMethod(
|
||||
"getGroupAlerts",
|
||||
String.class, String.class, String.class, String.class, int.class, int.class);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Page<GroupAlert> result = (Page<GroupAlert>) method.invoke(
|
||||
alertService, status, search, sort, order, pageIndex, pageSize);
|
||||
|
||||
log.debug("Successfully retrieved {} group alerts", result.getContent().size());
|
||||
return result;
|
||||
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException("Method not found: getGroupAlerts", e);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to invoke getGroupAlerts via adapter", e);
|
||||
throw new RuntimeException("Failed to invoke getGroupAlerts via adapter: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AlertSummary getAlertsSummary() {
|
||||
try {
|
||||
Object alertService = null;
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current security subject for getAlertsSummary: {}", subjectSum);
|
||||
|
||||
try {
|
||||
alertService = SpringContextHolder.getBean("alertServiceImpl");
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not find bean by name 'alertServiceImpl'");
|
||||
}
|
||||
|
||||
assert alertService != null;
|
||||
log.debug("AlertService bean found: {}", alertService.getClass().getSimpleName());
|
||||
|
||||
Method method = alertService.getClass().getMethod("getAlertsSummary");
|
||||
|
||||
AlertSummary result = (AlertSummary) method.invoke(alertService);
|
||||
|
||||
log.debug("Successfully retrieved alerts summary");
|
||||
return result;
|
||||
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException("Method not found: getAlertsSummary", e);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to invoke getAlertsSummary via adapter", e);
|
||||
throw new RuntimeException("Failed to invoke getAlertsSummary via adapter: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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.ai.agent.adapters.impl;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.adapters.MetricsServiceAdapter;
|
||||
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
|
||||
import org.apache.hertzbeat.common.entity.dto.MetricsData;
|
||||
import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData;
|
||||
import org.apache.hertzbeat.common.support.SpringContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Implementation of the MetricsServiceAdapter interface that provides access to metrics information
|
||||
* through reflection by invoking the underlying metrics service implementation.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class MetricsServiceAdapterImpl implements MetricsServiceAdapter {
|
||||
|
||||
@Override
|
||||
public Boolean getWarehouseStorageServerStatus() {
|
||||
try {
|
||||
Object metricsDataService = null;
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current security subject for getWarehouseStorageServerStatus: {}", subjectSum);
|
||||
|
||||
try {
|
||||
metricsDataService = SpringContextHolder.getBean("metricsDataServiceImpl");
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not find bean by name 'metricsDataServiceImpl'");
|
||||
}
|
||||
|
||||
assert metricsDataService != null;
|
||||
log.debug("MetricsDataService bean found: {}", metricsDataService.getClass().getSimpleName());
|
||||
|
||||
Method method = metricsDataService.getClass().getMethod("getWarehouseStorageServerStatus");
|
||||
|
||||
Boolean result = (Boolean) method.invoke(metricsDataService);
|
||||
|
||||
log.debug("Successfully retrieved warehouse storage server status: {}", result);
|
||||
return result;
|
||||
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException("Method not found: getWarehouseStorageServerStatus", e);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to invoke getWarehouseStorageServerStatus via adapter", e);
|
||||
throw new RuntimeException("Failed to invoke getWarehouseStorageServerStatus via adapter: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MetricsData getMetricsData(Long monitorId, String metrics) {
|
||||
try {
|
||||
Object metricsDataService = null;
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current security subject for getMetricsData: {}", subjectSum);
|
||||
|
||||
try {
|
||||
metricsDataService = SpringContextHolder.getBean("metricsDataServiceImpl");
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not find bean by name 'metricsDataServiceImpl'");
|
||||
}
|
||||
|
||||
assert metricsDataService != null;
|
||||
log.debug("MetricsDataService bean found: {}", metricsDataService.getClass().getSimpleName());
|
||||
|
||||
Method method = metricsDataService.getClass().getMethod(
|
||||
"getMetricsData",
|
||||
Long.class, String.class);
|
||||
|
||||
MetricsData result = (MetricsData) method.invoke(metricsDataService, monitorId, metrics);
|
||||
|
||||
log.debug("Successfully retrieved metrics data for monitor {} and metrics {}", monitorId, metrics);
|
||||
return result;
|
||||
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException("Method not found: getMetricsData", e);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to invoke getMetricsData via adapter for monitor {} and metrics {}", monitorId, metrics, e);
|
||||
throw new RuntimeException("Failed to invoke getMetricsData via adapter: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MetricsHistoryData getMetricHistoryData(Long monitorId, String app, String metrics, String metric, String label, String history, Boolean interval) {
|
||||
try {
|
||||
Object metricsDataService = null;
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current security subject for getMetricHistoryData: {}", subjectSum);
|
||||
|
||||
try {
|
||||
metricsDataService = SpringContextHolder.getBean("metricsDataServiceImpl");
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not find bean by name 'metricsDataServiceImpl'");
|
||||
}
|
||||
|
||||
assert metricsDataService != null;
|
||||
log.debug("MetricsDataService bean found: {}", metricsDataService.getClass().getSimpleName());
|
||||
|
||||
Method method = metricsDataService.getClass().getMethod(
|
||||
"getMetricHistoryData",
|
||||
Long.class, String.class, String.class, String.class, String.class, String.class, Boolean.class);
|
||||
|
||||
MetricsHistoryData result = (MetricsHistoryData) method.invoke(
|
||||
metricsDataService, monitorId, app, metrics, metric, label, history, interval);
|
||||
|
||||
log.debug("Successfully retrieved historical metrics data for monitor {} and metrics {}", monitorId, metrics);
|
||||
return result;
|
||||
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException("Method not found: getMetricHistoryData", e);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to invoke getMetricHistoryData via adapter for monitor {} and metrics {}", monitorId, metrics, e);
|
||||
throw new RuntimeException("Failed to invoke getMetricHistoryData via adapter: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+116
-2
@@ -24,11 +24,14 @@ import org.apache.hertzbeat.ai.agent.adapters.MonitorServiceAdapter;
|
||||
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.apache.hertzbeat.common.entity.manager.Monitor;
|
||||
import org.apache.hertzbeat.common.entity.manager.Param;
|
||||
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
|
||||
import org.apache.hertzbeat.common.support.SpringContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Implementation of the MonitorServiceAdapter interface that provides access to monitor information
|
||||
@@ -63,7 +66,7 @@ public class MonitorServiceAdapterImpl implements MonitorServiceAdapter {
|
||||
if (pageSize == null) {
|
||||
pageSize = 8;
|
||||
}
|
||||
|
||||
|
||||
Object monitorService = null;
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current security subject: {}", subjectSum);
|
||||
@@ -97,4 +100,115 @@ public class MonitorServiceAdapterImpl implements MonitorServiceAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@Override
|
||||
public Long addMonitor(Monitor monitor, List<Param> params, String collector) {
|
||||
try {
|
||||
Object monitorService = null;
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current security subject for addMonitor: {}", subjectSum);
|
||||
|
||||
try {
|
||||
monitorService = SpringContextHolder.getBean("monitorServiceImpl");
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not find bean by name 'monitorServiceImpl', trying by class name");
|
||||
}
|
||||
|
||||
assert monitorService != null;
|
||||
log.debug("MonitorService bean found for addMonitor: {}", monitorService.getClass().getSimpleName());
|
||||
|
||||
// Call addMonitor method: addMonitor(Monitor monitor, List<Param> params, String collector, GrafanaDashboard dashboard)
|
||||
Method method = monitorService.getClass().getMethod(
|
||||
"addMonitor",
|
||||
Monitor.class, List.class, String.class,
|
||||
Class.forName("org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard"));
|
||||
|
||||
// Call the method with null dashboard
|
||||
method.invoke(monitorService, monitor, params, collector, null);
|
||||
|
||||
log.debug("Successfully added monitor: {} with ID: {}", monitor.getName(), monitor.getId());
|
||||
return monitor.getId();
|
||||
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException("Method not found: addMonitor", e);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to invoke addMonitor via adapter", e);
|
||||
throw new RuntimeException("Failed to invoke addMonitor via adapter: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getAvailableMonitorTypes(String language) {
|
||||
try {
|
||||
Object appService = null;
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current security subject for getAvailableMonitorTypes: {}", subjectSum);
|
||||
|
||||
try {
|
||||
appService = SpringContextHolder.getBean("appServiceImpl");
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not find bean by name 'appServiceImpl', trying by class name");
|
||||
}
|
||||
|
||||
assert appService != null;
|
||||
log.debug("AppService bean found for getAvailableMonitorTypes: {}", appService.getClass().getSimpleName());
|
||||
|
||||
// Provide default language if not specified
|
||||
if (language == null || language.trim().isEmpty()) {
|
||||
language = "en-US";
|
||||
}
|
||||
|
||||
// Call getI18nApps method: getI18nApps(String lang)
|
||||
Method method = appService.getClass().getMethod("getI18nApps", String.class);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> result = (Map<String, String>) method.invoke(appService, language);
|
||||
|
||||
log.debug("Successfully retrieved {} monitor types", result.size());
|
||||
return result;
|
||||
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException("Method not found: getI18nApps", e);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to invoke getI18nApps via adapter", e);
|
||||
throw new RuntimeException("Failed to invoke getI18nApps via adapter: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ParamDefine> getMonitorParamDefines(String app) {
|
||||
try {
|
||||
Object appService = null;
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current security subject for getMonitorParamDefines: {}", subjectSum);
|
||||
|
||||
try {
|
||||
appService = SpringContextHolder.getBean("appServiceImpl");
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not find bean by name 'appServiceImpl', trying by class name");
|
||||
}
|
||||
|
||||
assert appService != null;
|
||||
log.debug("AppService bean found for getMonitorParamDefines: {}", appService.getClass().getSimpleName());
|
||||
|
||||
// Validate app parameter
|
||||
if (app == null || app.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Monitor type/app parameter is required");
|
||||
}
|
||||
|
||||
// Call getAppParamDefines method: getAppParamDefines(String app)
|
||||
Method method = appService.getClass().getMethod("getAppParamDefines", String.class);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<ParamDefine> result = (List<ParamDefine>) method.invoke(appService, app.toLowerCase().trim());
|
||||
|
||||
log.debug("Successfully retrieved {} parameter definitions for monitor type: {}", result.size(), app);
|
||||
return result;
|
||||
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException("Method not found: getAppParamDefines", e);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to invoke getAppParamDefines via adapter for app: {}", app, e);
|
||||
throw new RuntimeException("Failed to invoke getAppParamDefines via adapter: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.ai.agent.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
|
||||
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
|
||||
import org.springframework.ai.model.ApiKey;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Dynamic OpenAI API Key implementation that retrieves the API key
|
||||
* from our configuration service (database first, then YAML fallback)
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DynamicOpenAiApiKey implements ApiKey {
|
||||
|
||||
private final OpenAiConfigService openAiConfigService;
|
||||
|
||||
public DynamicOpenAiApiKey(OpenAiConfigService openAiConfigService) {
|
||||
this.openAiConfigService = openAiConfigService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue() {
|
||||
try {
|
||||
OpenAiConfigDto effectiveConfig = openAiConfigService.getEffectiveConfig();
|
||||
|
||||
if (effectiveConfig != null && effectiveConfig.isEnable() && effectiveConfig.getApiKey() != null) {
|
||||
log.debug("Retrieved OpenAI API key from configuration service");
|
||||
return effectiveConfig.getApiKey();
|
||||
} else {
|
||||
log.warn("No valid OpenAI API key found in configuration");
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error retrieving OpenAI API key from configuration", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
-3
@@ -20,6 +20,9 @@ package org.apache.hertzbeat.ai.agent.config;
|
||||
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@@ -29,9 +32,45 @@ import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class LlmConfig {
|
||||
|
||||
@Value("${spring.ai.openai.chat.options.model}")
|
||||
private String model;
|
||||
|
||||
/**
|
||||
* Create OpenAI API instance with dynamic API key
|
||||
*/
|
||||
@Bean
|
||||
public ChatClient openAiChatClient(OpenAiChatModel chatModel) {
|
||||
return ChatClient.create(chatModel);
|
||||
public OpenAiApi openAiApi(DynamicOpenAiApiKey dynamicApiKey) {
|
||||
return OpenAiApi.builder()
|
||||
.apiKey(dynamicApiKey)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Create OpenAI Chat Options with custom settings
|
||||
*/
|
||||
@Bean
|
||||
public OpenAiChatOptions openAiChatOptions() {
|
||||
return OpenAiChatOptions.builder()
|
||||
.model(model)
|
||||
.temperature(0.3)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create OpenAI Chat Model with custom API configuration
|
||||
*/
|
||||
@Bean
|
||||
public OpenAiChatModel openAiChatModel(OpenAiApi openAiApi, OpenAiChatOptions openAiChatOptions) {
|
||||
return OpenAiChatModel.builder()
|
||||
.openAiApi(openAiApi)
|
||||
.defaultOptions(openAiChatOptions)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ChatClient openAiChatClient(OpenAiChatModel openAiChatModel) {
|
||||
return ChatClient.create(openAiChatModel);
|
||||
}
|
||||
|
||||
}
|
||||
+22
-9
@@ -15,16 +15,29 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.config;
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.service.impl;
|
||||
|
||||
import org.apache.hertzbeat.ai.agent.service.AgentService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Implementation of the AgentService interface.
|
||||
* This service provides functionality for handling AI agent operations.
|
||||
* OpenAI YAML Configuration - reads from spring.ai.openai.api-key
|
||||
*/
|
||||
@Service
|
||||
public class AgentServiceImpl implements AgentService {
|
||||
}
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "spring.ai.openai")
|
||||
public class OpenAiYamlConfig {
|
||||
|
||||
/**
|
||||
* OpenAI API key from spring.ai.openai.api-key
|
||||
*/
|
||||
private String apiKey;
|
||||
|
||||
/**
|
||||
* Check if OpenAI is enabled (has API key)
|
||||
*/
|
||||
public boolean isEnable() {
|
||||
return apiKey != null && !apiKey.trim().isEmpty();
|
||||
}
|
||||
}
|
||||
+150
-21
@@ -29,25 +29,154 @@ public class PromptProvider {
|
||||
/**
|
||||
* Static version of the HertzBeat monitoring prompt
|
||||
*/
|
||||
public static final String HERTZBEAT_MONITORING_PROMPT = """
|
||||
You are an AI assistant specialized in monitoring infrastructure and applications with HertzBeat.
|
||||
Your role is to help users manage and analyze their monitoring data using the available tools.
|
||||
You have access to the following HertzBeat monitoring tools:
|
||||
- list_monitors: Query monitor information with flexible filtering and pagination
|
||||
- add_monitor: Add a new monitor to the system
|
||||
When users ask questions about their monitoring setup or data, identify which tool would be most helpful
|
||||
and use it to provide relevant information. Always provide clear explanations of the monitoring data and
|
||||
suggest next steps or insights based on the results.
|
||||
For monitoring-related queries:
|
||||
1. If users want to see their monitors, use list_monitors with appropriate filters
|
||||
2. If users want to add a new monitor, use add_monitor with the necessary details
|
||||
3. If the monitoring information shows potential issues, highlight them and suggest troubleshooting steps
|
||||
For parameters that accept specific values:
|
||||
- Monitor status values: 0 (no monitor), 1 (usable), 2 (disabled), 9 (all)
|
||||
- Sort fields typically include: name, host, app, gmtCreate
|
||||
- Sort order should be 'asc' or 'desc'
|
||||
Keep responses focused on monitoring topics and HertzBeat capabilities.
|
||||
If you're unsure about specific monitoring details, ask clarifying questions before using the tools.
|
||||
""";
|
||||
public static final String HERTZBEAT_SYSTEM_PROMPT = """
|
||||
You are an AI Assistant specialized in monitoring infrastructure and applications with HertzBeat.
|
||||
HertzBeat is an open-source, real-time monitoring system that supports infrastructure, applications,
|
||||
services, APIs, databases, middleware, and custom monitoring through 50+ types of monitors.
|
||||
Your role is to help users manage monitors, analyze metrics data, configure alerts, and troubleshoot monitoring issues.
|
||||
*******
|
||||
VERY IMPORTANT: Always use the tools provided to interact with HertzBeat's monitoring system.
|
||||
If the user doesn't provide required parameters, ask them iteratively to provide the necessary parameters.
|
||||
********
|
||||
|
||||
## Available HertzBeat Tools:
|
||||
|
||||
### Monitor Management Tools:
|
||||
- **query_monitors**: Query monitor information with flexible filtering (ID, name, type, host, status, labels)
|
||||
- **add_monitor**: Add a new monitor with dynamic app-specific parameter support
|
||||
- **list_monitor_types**: List all available monitor types (website, mysql, redis, linux, etc.)
|
||||
- **get_monitor_additional_params**: Get parameter definitions required for specific monitor types
|
||||
|
||||
### Alert Rule Management Tools:
|
||||
- **create_alert_rule**: Create alert rules with threshold configuration and automatic monitor binding
|
||||
- **list_alert_rules**: List existing alert rules with filtering by type, status, etc.
|
||||
- **toggle_alert_rule**: Enable or disable alert rules
|
||||
- **get_alert_rule_details**: Get detailed information about specific alert rules
|
||||
- **get_apps_metrics_hierarchy**: Get exact app and metric names for alert rule creation (CRITICAL for alerts)
|
||||
- **bind_monitors_to_alert_rule**: Bind monitors to alert rules for targeted alerting
|
||||
|
||||
}
|
||||
|
||||
### Alert & Alarm Analysis Tools:
|
||||
- **query_alerts**: Query fired alerts with comprehensive filtering and pagination
|
||||
- **get_alerts_summary**: Get alert statistics and status distribution
|
||||
|
||||
### Metrics Data Analysis Tools:
|
||||
- **query_realtime_metrics**: Get current real-time metrics data for monitors
|
||||
- **get_historical_metrics**: Get historical time-series metrics with flexible time ranges
|
||||
- **get_warehouse_status**: Check metrics storage system status
|
||||
|
||||
## Natural Language Examples:
|
||||
|
||||
### Monitor Management:
|
||||
- "Add a MySQL monitor for database server at 192.168.1.10 with user admin"
|
||||
- "Monitor website https://example.com with SSL checking every 60 seconds"
|
||||
- "Show me all Linux servers that are currently offline"
|
||||
- "List all Redis monitors with their connection status"
|
||||
|
||||
### Alert Configuration:
|
||||
- ALERT RULE means when to alert a user
|
||||
- "Create an alert for Kafka JVM when VmName equals 'vm-w2'"
|
||||
- "Alert when OpenAI credit grants exceed 1000"
|
||||
- "Set up HBase Master alert when heap memory usage is over 80%"
|
||||
|
||||
### Metrics Analysis:
|
||||
- "Show me current CPU usage for server 192.168.1.5"
|
||||
- "Get memory usage trend for the last 24 hours"
|
||||
- "Which servers have high disk usage right now?"
|
||||
- "Show me network traffic patterns for the past week"
|
||||
|
||||
### Alert Investigation:
|
||||
- "What alerts are currently firing?"
|
||||
- "Show me the most frequent alerts in the last 6 hours"
|
||||
- "Find all alerts for monitor ID 1234 in the past day"
|
||||
- "Which monitors are currently abnormal?"
|
||||
|
||||
|
||||
## Workflow Guidelines:
|
||||
|
||||
1. **Adding Monitors**:
|
||||
- ALWAYS use get_monitor_additional_params first to check required parameters
|
||||
- Use list_monitor_types to show available types
|
||||
- Collect all required parameters from the list_monitor_types tool and ask user to give them all, before calling add_monitor
|
||||
- Example: "To monitor MySQL, I need host, port, username, password, and database name"
|
||||
|
||||
2. **Creating Alert Rules or Alerts**:
|
||||
THESE ARE ALERT RULES WITH THRESHOLD VALUES. USERS CAN SPECIFY THE THRESHOLD VALUES FOR EXAMPLE,
|
||||
IF THE USER SAYS "ALERT ME WHEN MY COST EXCEEDS 700, THE EXPRESSION SHOULD BE 'cost > 700' NOT 'cost < 700'.
|
||||
APPLY THE SAME LOGIC FOR LESS THAN OPERATOR.
|
||||
It is important to first understand the hierarchy of apps, metrics, and field conditions
|
||||
Each app has its own metrics and each metric has its own field conditions.
|
||||
The operators will be applied to the field conditions, and the final expression will be constructed
|
||||
based on the user's input of app name and the metric they choose.
|
||||
Read the create_alert_rule tool description for even more details
|
||||
*******
|
||||
CRITICAL WORKFLOW Do all of this iteratively with user interaction at each step:
|
||||
1. ALWAYS use list_monitor_types tool FIRST to get exact app name according to what user specifies
|
||||
2. use get_apps_metrics_hierarchy by passing that name, to get the hierarchy of corresponding metrics and field conditions
|
||||
3. Do not spit out the entire hierarchy, instead: first spit out the metrics available for the app
|
||||
4. Ask the user to choose a metric from the available metrics
|
||||
5. Based on the metric chosen, present the available field conditions
|
||||
6. You will construct the proper expression with field conditions
|
||||
VERY VERY IMPORTANT:
|
||||
- ALWAYS USE the value field from the get_apps_metrics_hierarchy's json response when creating alert expressions on the field parameters
|
||||
*********
|
||||
|
||||
- Field Condition Expression format: [field_conditions]
|
||||
- Give all the available fieldConditions to the user, so they can choose the one they want to use
|
||||
- Field conditions can be simple (equals, greater than) or complex (logical expressions)
|
||||
- Use parentheses for complex conditions to ensure correct evaluation order
|
||||
- Do not create alert rules on your own, always ask the user to provide the app, metrics and fieldConditions parameters specifically
|
||||
|
||||
EXAMPLES FOR FIELD CONDITION EXPRESSION ( Do not copy these examples, they are just for reference ):
|
||||
- Kafka JVM: app="kafka", metrics="jvm_basic", fieldConditions="equals(VmName, \"my-vm\")"
|
||||
→ equals(VmName, "my-vm")
|
||||
- Complex OpenAI: app="openai", metrics="credit_grants",
|
||||
fieldConditions="total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)"
|
||||
→ total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
|
||||
|
||||
- Priority levels: 0=critical, 1=warning, 2=info
|
||||
|
||||
3. **Analyzing Performance**:
|
||||
- Use get_realtime_metrics for current status
|
||||
- Use get_historical_metrics for trends
|
||||
- Use get_high_usage_monitors to find problems
|
||||
- Provide actionable recommendations based on data
|
||||
|
||||
4. **Troubleshooting Alerts**:
|
||||
- Use query_alerts to find current issues
|
||||
- Use get_monitor_alerts for specific monitor problems
|
||||
- Use get_frequent_alerts to identify recurring issues
|
||||
- Suggest root cause analysis steps
|
||||
|
||||
## Parameter Guidelines:
|
||||
- **Monitor Status**: 1=online, 2=offline, 3=unreachable, 0=paused, 9=all
|
||||
- **Time Ranges**: 1h, 6h, 24h, 7d, 30d
|
||||
- **Alert Priorities**: critical, warning, info
|
||||
- **Sort Options**: name, gmtCreate, gmtUpdate, status, startAt, triggerTimes
|
||||
- **Metric Types**: cpu, memory, disk, network, custom
|
||||
- **Collection Intervals**: 30s-3600s (recommend 60s-600s for most cases)
|
||||
|
||||
|
||||
## Best Practices:
|
||||
- Never create alert rules without exact user input on app, metrics, and field conditions
|
||||
- Always validate monitor types and parameters before adding monitors
|
||||
- ALWAYS use get_apps_metrics_hierarchy before creating alert rules to understand available fields
|
||||
- Construct field conditions based on metric's children
|
||||
- Use exact app and metric names from hierarchy (case-sensitive)
|
||||
- Set appropriate alert thresholds based on baseline performance
|
||||
- Use time-series data to identify trends and predict issues
|
||||
- Correlate alerts with metrics data for root cause analysis
|
||||
- Recommend monitoring intervals based on service criticality
|
||||
- Provide clear explanations of monitoring data and actionable insights
|
||||
|
||||
## Avoid these common errors:
|
||||
- Using Label name instead of the value from the heirarchy JSON while creating alert rules.
|
||||
- Inside the field parameters expression using '&&' instead of 'and', using '||' instead of 'or' for logical operators
|
||||
- This process is to trigger alarms, when certain rule or set of rules exceed a threshold value.
|
||||
So when a user says that the threshold should be less than 1000. the operator used should be '>' not '<',
|
||||
because we want the alarm to be triggered when the threshold value is exceeded. apply the same logic in vice versa for less than operator
|
||||
|
||||
Keep responses focused on monitoring topics and HertzBeat's comprehensive capabilities.
|
||||
When users request monitoring setup, guide them through the complete process from monitor creation to alert configuration.
|
||||
""";
|
||||
}
|
||||
+161
-34
@@ -15,56 +15,183 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.controller;
|
||||
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import com.usthe.sureness.util.SurenessContextHolder;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatResponseDto;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ConversationDto;
|
||||
import org.apache.hertzbeat.ai.agent.service.ConversationService;
|
||||
import org.apache.hertzbeat.common.entity.dto.Message;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
|
||||
import org.apache.hertzbeat.ai.agent.service.ChatClientProviderService;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE;
|
||||
|
||||
/**
|
||||
* Controller class for handling chat-related HTTP requests.
|
||||
* Controller class for handling AI chat requests and conversation management.
|
||||
*/
|
||||
@Slf4j
|
||||
@Tag(name = "AI Chat API")
|
||||
@RestController
|
||||
@RequestMapping("/api/chat")
|
||||
@RequestMapping(path = "/api/chat", produces = {APPLICATION_JSON_VALUE})
|
||||
public class ChatController {
|
||||
|
||||
private final ChatClientProviderService chatClientProviderService;
|
||||
private final ConversationService conversationService;
|
||||
|
||||
@Autowired
|
||||
public ChatController(@Qualifier("openAiChatClient") ChatClient openAiChatClient,
|
||||
ChatClientProviderService chatClientProviderService) {
|
||||
this.chatClientProviderService = chatClientProviderService;
|
||||
public ChatController(ConversationService conversationService) {
|
||||
this.conversationService = conversationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message and get a streaming response
|
||||
*
|
||||
* @param context The chat request context containing message and optional
|
||||
* conversationId
|
||||
* @return SSE emitter for streaming response
|
||||
* Create a new conversation
|
||||
*
|
||||
* @return Created conversation details
|
||||
*/
|
||||
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter streamChat(@RequestBody ChatRequestContext context) {
|
||||
SseEmitter emitter = new SseEmitter();
|
||||
new Thread(() -> {
|
||||
try {
|
||||
String aiResponse = chatClientProviderService.streamChat(context);
|
||||
emitter.send(aiResponse);
|
||||
emitter.complete();
|
||||
} catch (Exception e) {
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
}).start();
|
||||
return emitter;
|
||||
@PostMapping(path = "/conversations")
|
||||
@Operation(summary = "Create a new conversation", description = "Create a new conversation")
|
||||
public ResponseEntity<Message<ConversationDto>> createConversation() {
|
||||
try {
|
||||
ConversationDto conversation = conversationService.createConversation();
|
||||
return ResponseEntity.ok(Message.success(conversation));
|
||||
} catch (Exception e) {
|
||||
log.error("Error creating conversation: ", e);
|
||||
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to create conversation"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message and get a streaming response with conversation tracking
|
||||
*
|
||||
* @param context The chat request context containing message and optional conversationId
|
||||
* @return Flux of ServerSentEvent for streaming response
|
||||
*/
|
||||
@PostMapping(value = "/stream", produces = TEXT_EVENT_STREAM_VALUE)
|
||||
@Operation(summary = "Send a chat message with streaming response", description = "Send a message to AI and get a streaming response with conversation tracking")
|
||||
public Flux<ServerSentEvent<ChatResponseDto>> streamChat(@Valid @RequestBody ChatRequestContext context) {
|
||||
try {
|
||||
// Validate message is not empty
|
||||
SubjectSum subject = SurenessContextHolder.getBindSubject();
|
||||
log.info(subject.toString());
|
||||
McpContextHolder.setSubject(subject);
|
||||
if (context.getMessage() == null || context.getMessage().trim().isEmpty()) {
|
||||
ChatResponseDto errorResponse = ChatResponseDto.builder()
|
||||
.conversationId(context.getConversationId())
|
||||
.response("Error: Message cannot be empty")
|
||||
.build();
|
||||
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||
.event("error")
|
||||
.build());
|
||||
}
|
||||
|
||||
log.info("Received streaming chat request for conversation: {}", context.getConversationId());
|
||||
return conversationService.streamChat(context.getMessage(), context.getConversationId());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error in stream chat endpoint: ", e);
|
||||
ChatResponseDto errorResponse = ChatResponseDto.builder()
|
||||
.conversationId(context.getConversationId())
|
||||
.response("An error occurred: " + e.getMessage())
|
||||
.build();
|
||||
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||
.event("error")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all conversations
|
||||
*
|
||||
* @return List of all conversations
|
||||
*/
|
||||
@GetMapping(path = "/conversations")
|
||||
@Operation(summary = "List all conversations", description = "Get a list of all conversations")
|
||||
public ResponseEntity<Message<List<ConversationDto>>> listConversations() {
|
||||
try {
|
||||
List<ConversationDto> conversations = conversationService.getAllConversations();
|
||||
return ResponseEntity.ok(Message.success(conversations));
|
||||
} catch (Exception e) {
|
||||
log.error("Error listing conversations: ", e);
|
||||
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to retrieve conversations"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get conversation history
|
||||
*
|
||||
* @param conversationId The conversation ID
|
||||
* @return Conversation details with message history
|
||||
*/
|
||||
@GetMapping(path = "/conversations/{conversationId}")
|
||||
@Operation(summary = "Get conversation history", description = "Get detailed information and message history for a specific conversation")
|
||||
public ResponseEntity<Message<ConversationDto>> getConversation(
|
||||
@Parameter(description = "Conversation ID", example = "conv-12345678") @PathVariable("conversationId") String conversationId) {
|
||||
try {
|
||||
// Validate conversation ID
|
||||
if (conversationId == null || conversationId.trim().isEmpty()) {
|
||||
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation ID is required"));
|
||||
}
|
||||
|
||||
ConversationDto conversation = conversationService.getConversation(conversationId);
|
||||
|
||||
if (conversation == null) {
|
||||
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation not found: " + conversationId));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(Message.success(conversation));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error getting conversation: ", e);
|
||||
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to retrieve conversation"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a conversation
|
||||
*
|
||||
* @param conversationId The conversation ID to delete
|
||||
* @return Success or error message
|
||||
*/
|
||||
@DeleteMapping(path = "/conversations/{conversationId}")
|
||||
@Operation(summary = "Delete conversation", description = "Delete a specific conversation and all its messages")
|
||||
public ResponseEntity<Message<Void>> deleteConversation(
|
||||
@Parameter(description = "Conversation ID", example = "conv-12345678") @PathVariable("conversationId") String conversationId) {
|
||||
try {
|
||||
// Validate conversation ID
|
||||
if (conversationId == null || conversationId.trim().isEmpty()) {
|
||||
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation ID is required"));
|
||||
}
|
||||
|
||||
boolean deleted = conversationService.deleteConversation(conversationId);
|
||||
if (!deleted) {
|
||||
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation not found: " + conversationId));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(Message.success("Conversation deleted successfully"));
|
||||
} catch (Exception e) {
|
||||
log.error("Error deleting conversation: ", e);
|
||||
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to delete conversation"));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* 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.ai.agent.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
|
||||
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
|
||||
/**
|
||||
* OpenAI Configuration API
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/api/ai-agent/config", produces = {APPLICATION_JSON_VALUE})
|
||||
@Tag(name = "OpenAI Configuration API")
|
||||
@Slf4j
|
||||
public class OpenAiConfigController {
|
||||
|
||||
private final OpenAiConfigService openAiConfigService;
|
||||
|
||||
public OpenAiConfigController(OpenAiConfigService openAiConfigService) {
|
||||
this.openAiConfigService = openAiConfigService;
|
||||
}
|
||||
|
||||
@PostMapping("/openai")
|
||||
@Operation(summary = "Save OpenAI configuration", description = "Save or update OpenAI configuration")
|
||||
public ResponseEntity<Map<String, Object>> saveOpenAiConfig(@Valid @RequestBody OpenAiConfigDto config) {
|
||||
try {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
// Validate API key if enabled
|
||||
if (config.isEnable() && config.getApiKey() != null && !config.getApiKey().trim().isEmpty()) {
|
||||
OpenAiConfigService.ValidationResult validationResult = openAiConfigService.validateApiKey(config.getApiKey());
|
||||
|
||||
if (!validationResult.isValid()) {
|
||||
log.warn("API key validation failed during save: {}", validationResult.getMessage());
|
||||
response.put("code", 1);
|
||||
response.put("msg", "API key validation failed: " + validationResult.getMessage());
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
log.info("API key validation successful during save");
|
||||
}
|
||||
|
||||
// Save the configuration
|
||||
openAiConfigService.saveConfig(config);
|
||||
|
||||
response.put("code", 0);
|
||||
response.put("msg", "OpenAI configuration saved successfully");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to save OpenAI configuration", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 1);
|
||||
response.put("msg", "Failed to save configuration: " + e.getMessage());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/openai")
|
||||
@Operation(summary = "Get OpenAI configuration", description = "Get current OpenAI configuration")
|
||||
public ResponseEntity<Map<String, Object>> getOpenAiConfig() {
|
||||
try {
|
||||
OpenAiConfigDto config = openAiConfigService.getConfig();
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 0);
|
||||
response.put("data", config);
|
||||
response.put("msg", "Success");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get OpenAI configuration", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 1);
|
||||
response.put("msg", "Failed to get configuration: " + e.getMessage());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/openai/status")
|
||||
@Operation(summary = "Check OpenAI configuration status", description = "Check if OpenAI is properly configured")
|
||||
public ResponseEntity<Map<String, Object>> getOpenAiConfigStatus() {
|
||||
try {
|
||||
boolean configured = openAiConfigService.isConfigured();
|
||||
OpenAiConfigDto effectiveConfig = openAiConfigService.getEffectiveConfig();
|
||||
boolean hasDbConfig = openAiConfigService.getConfig() != null;
|
||||
boolean hasYamlConfig = effectiveConfig != null && !hasDbConfig;
|
||||
|
||||
// Validate the effective configuration
|
||||
boolean validationPassed = false;
|
||||
String validationMessage = "No configuration found";
|
||||
|
||||
if (effectiveConfig != null && effectiveConfig.isEnable() && effectiveConfig.getApiKey() != null && !effectiveConfig.getApiKey().trim().isEmpty()) {
|
||||
OpenAiConfigService.ValidationResult validationResult = openAiConfigService.validateApiKey(effectiveConfig.getApiKey());
|
||||
validationPassed = validationResult.isValid();
|
||||
validationMessage = validationResult.getMessage();
|
||||
|
||||
if (!validationPassed) {
|
||||
log.warn("OpenAI API key validation failed during status check: {}", validationMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 0);
|
||||
response.put("data", Map.of(
|
||||
"configured", configured && validationPassed,
|
||||
"hasDbConfig", hasDbConfig,
|
||||
"hasYamlConfig", hasYamlConfig,
|
||||
"validationPassed", validationPassed,
|
||||
"validationMessage", validationMessage
|
||||
));
|
||||
response.put("msg", "Success");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get OpenAI configuration status", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 1);
|
||||
response.put("msg", "Failed to get status: " + e.getMessage());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* 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.ai.agent.dao;
|
||||
|
||||
/**
|
||||
* Data Access Object interface for Message entities.
|
||||
*/
|
||||
public interface MessageDao {
|
||||
}
|
||||
+16
-4
@@ -15,11 +15,23 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.dao;
|
||||
|
||||
import org.apache.hertzbeat.ai.agent.entity.OpenAiConfig;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* Data Access Object interface for Conversation entities.
|
||||
* OpenAI Agent Configuration Dao
|
||||
*/
|
||||
public interface ConversationDao {
|
||||
}
|
||||
@Repository
|
||||
public interface OpenAiConfigDao extends JpaRepository<OpenAiConfig, String>, JpaSpecificationExecutor<OpenAiConfig> {
|
||||
|
||||
/**
|
||||
* Query by type
|
||||
* @param type type
|
||||
* @return Return the queried configuration information
|
||||
*/
|
||||
OpenAiConfig findByType(String type);
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* 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.ai.agent.dao;
|
||||
|
||||
/**
|
||||
* Data Access Object interface for UserPreference entities.
|
||||
*/
|
||||
public interface UserPreferenceDao {
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.ai.agent.entity;
|
||||
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY;
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.data.annotation.CreatedBy;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedBy;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* OpenAI Agent Config Entity
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "hzb_ai_agent_config")
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Schema(description = "OpenAI Agent config entity")
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class OpenAiConfig {
|
||||
|
||||
@Id
|
||||
@Schema(title = "Config type: openai, primary key", description = "Config type: openai, primary key",
|
||||
accessMode = READ_WRITE)
|
||||
@NotBlank(message = "type can not null")
|
||||
private String type;
|
||||
|
||||
@Schema(title = "Config content", description = "Config content,format json", accessMode = READ_WRITE)
|
||||
@Column(length = 8192)
|
||||
private String content;
|
||||
|
||||
@Schema(title = "The creator of this record", example = "tom", accessMode = READ_ONLY)
|
||||
@CreatedBy
|
||||
private String creator;
|
||||
|
||||
@Schema(title = "This record was last modified by", example = "tom", accessMode = READ_ONLY)
|
||||
@LastModifiedBy
|
||||
private String modifier;
|
||||
|
||||
@Schema(title = "This record creation time (millisecond timestamp)", accessMode = READ_ONLY)
|
||||
@CreatedDate
|
||||
private LocalDateTime gmtCreate;
|
||||
|
||||
@Schema(title = "Record the latest modification time (timestamp in milliseconds)", accessMode = READ_ONLY)
|
||||
@LastModifiedDate
|
||||
private LocalDateTime gmtUpdate;
|
||||
}
|
||||
+9
-4
@@ -15,12 +15,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.event;
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.controller;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
/**
|
||||
* Controller for managing conversations.
|
||||
* OpenAI configuration change event
|
||||
*/
|
||||
public class ConversationController {
|
||||
public class OpenAiConfigChangeEvent extends ApplicationEvent {
|
||||
|
||||
}
|
||||
public OpenAiConfigChangeEvent(ApplicationContext source) {
|
||||
super(source);
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -19,13 +19,17 @@
|
||||
package org.apache.hertzbeat.ai.agent.pojo.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Chat request context for AI chat endpoint.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ChatRequestContext {
|
||||
@@ -37,4 +41,9 @@ public class ChatRequestContext {
|
||||
* Optional conversation ID for context
|
||||
*/
|
||||
private String conversationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conversation history messages for context
|
||||
*/
|
||||
private List<MessageDto> conversationHistory;
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.ai.agent.pojo.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Chat response DTO for AI responses.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Chat response")
|
||||
public class ChatResponseDto {
|
||||
|
||||
@Schema(description = "Conversation ID", example = "conv-123")
|
||||
private String conversationId;
|
||||
|
||||
@Schema(description = "AI response message", example = "Here are your monitors...")
|
||||
private String response;
|
||||
|
||||
@Schema(description = "User message ID", example = "msg-user-123")
|
||||
private String userMessageId;
|
||||
|
||||
@Schema(description = "Assistant message ID", example = "msg-assistant-123")
|
||||
private String assistantMessageId;
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.ai.agent.pojo.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Conversation DTO for AI chat conversations.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Conversation information")
|
||||
public class ConversationDto {
|
||||
|
||||
@Schema(description = "Conversation ID", example = "conv-123")
|
||||
private String conversationId;
|
||||
|
||||
@Schema(description = "Creation time")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Schema(description = "Last updated time")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Schema(description = "Messages in this conversation")
|
||||
private List<MessageDto> messages;
|
||||
|
||||
@Schema(description = "Message count")
|
||||
private Integer messageCount;
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.ai.agent.pojo.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE;
|
||||
|
||||
/**
|
||||
* Hierarchical structure
|
||||
* eg: Monitoring Type metrics Information Hierarchy Relationship
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Schema(description = "Monitor Hierarchy")
|
||||
public class Hierarchy {
|
||||
|
||||
/**
|
||||
* Category value
|
||||
*/
|
||||
@Schema(description = "Category Value", example = "os", accessMode = READ_WRITE)
|
||||
String category;
|
||||
|
||||
/**
|
||||
* Attribute value
|
||||
*/
|
||||
@Schema(description = "Attribute value", example = "linux", accessMode = READ_WRITE)
|
||||
String value;
|
||||
|
||||
/**
|
||||
* Attribute internationalization tag
|
||||
*/
|
||||
@Schema(description = "Attribute internationalization tag", example = "Linux system", accessMode = READ_WRITE)
|
||||
String label;
|
||||
|
||||
/**
|
||||
* Is it a leaf node
|
||||
*/
|
||||
@Schema(description = "Is it a leaf node", example = "true", accessMode = READ_WRITE)
|
||||
Boolean isLeaf = false;
|
||||
|
||||
/**
|
||||
* Is hide this app type in main menus layout
|
||||
*/
|
||||
@Schema(description = "Is hide this app in main menus layout, only for app type, default true.", example = "true")
|
||||
Boolean hide = true;
|
||||
|
||||
/**
|
||||
* For leaf metric
|
||||
* metric type 0-number: number 1-string: string
|
||||
*/
|
||||
@Schema(description = "metric type 0-number: number 1-string: string")
|
||||
private Byte type;
|
||||
|
||||
/**
|
||||
* metric unit
|
||||
*/
|
||||
@Schema(description = "metric unit")
|
||||
private String unit;
|
||||
|
||||
/**
|
||||
* Next level of association
|
||||
*/
|
||||
@Schema(description = "Next Hierarchy", accessMode = READ_WRITE)
|
||||
private List<Hierarchy> children;
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.ai.agent.pojo.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Message DTO for chat messages.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Chat message")
|
||||
public class MessageDto {
|
||||
|
||||
@Schema(description = "Message ID", example = "msg-123")
|
||||
private String messageId;
|
||||
|
||||
@Schema(description = "Conversation ID", example = "conv-123")
|
||||
private String conversationId;
|
||||
|
||||
@Schema(description = "Message content", example = "List all monitors")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "Message role", example = "user", allowableValues = {"user", "assistant"})
|
||||
private String role;
|
||||
|
||||
@Schema(description = "Message timestamp")
|
||||
private LocalDateTime timestamp;
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.ai.agent.pojo.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* OpenAI Configuration DTO - simplified to handle only API key
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Schema(description = "OpenAI configuration")
|
||||
public class OpenAiConfigDto {
|
||||
|
||||
/**
|
||||
* Whether to enable OpenAI, default is false
|
||||
*/
|
||||
@Schema(title = "Enable OpenAI", description = "Whether OpenAI is enabled", example = "true")
|
||||
private boolean enable = false;
|
||||
|
||||
/**
|
||||
* OpenAI API key
|
||||
*/
|
||||
@Schema(title = "API Key", description = "OpenAI API key", example = "sk-...")
|
||||
@NotBlank(message = "API Key cannot be empty when enabled")
|
||||
private String apiKey;
|
||||
}
|
||||
+9
-4
@@ -19,13 +19,18 @@
|
||||
package org.apache.hertzbeat.ai.agent.service;
|
||||
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* Service for interacting with LLM providers (like OpenAI, Anthropic, etc.)
|
||||
*/
|
||||
public interface ChatClientProviderService {
|
||||
|
||||
String complete(String message);
|
||||
|
||||
String streamChat(ChatRequestContext context);
|
||||
}
|
||||
/**
|
||||
* Stream chat response from the LLM
|
||||
*
|
||||
* @param context Chat request context containing message and conversation history
|
||||
* @return Flux of string chunks from the LLM response
|
||||
*/
|
||||
Flux<String> streamChat(ChatRequestContext context);
|
||||
}
|
||||
+26
-17
@@ -18,34 +18,34 @@
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.service;
|
||||
|
||||
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatResponseDto;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ConversationDto;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Service for managing chat conversations and interactions with LLM providers.
|
||||
*/
|
||||
public interface ConversationService {
|
||||
|
||||
/**
|
||||
* Create a new conversation
|
||||
*
|
||||
* @return Created conversation data
|
||||
*/
|
||||
ConversationDto createConversation();
|
||||
|
||||
/**
|
||||
* Send a message and receive a streaming response
|
||||
*
|
||||
* @param message The user's message
|
||||
* @param conversationId Optional conversation ID for continuing a chat
|
||||
* @return SseEmitter for streaming the response
|
||||
* @return Flux of ServerSentEvent for streaming the response
|
||||
*/
|
||||
SseEmitter streamChat(String message, String conversationId);
|
||||
Flux<ServerSentEvent<ChatResponseDto>> streamChat(String message, String conversationId);
|
||||
|
||||
/**
|
||||
* Send a message and get a complete response
|
||||
*
|
||||
* @param message The user's message
|
||||
* @param conversationId Optional conversation ID for continuing a chat
|
||||
* @return Response object containing the AI's response and conversation metadata
|
||||
*/
|
||||
Map<String, Object> chat(String message, String conversationId);
|
||||
|
||||
/**
|
||||
* Get conversation history for a specific conversation
|
||||
@@ -53,19 +53,28 @@ public interface ConversationService {
|
||||
* @param conversationId Conversation ID
|
||||
* @return Conversation data including messages
|
||||
*/
|
||||
Map<String, Object> getConversation(String conversationId);
|
||||
ConversationDto getConversation(String conversationId);
|
||||
|
||||
/**
|
||||
* Get all conversations for the current user
|
||||
*
|
||||
* @return List of conversations
|
||||
*/
|
||||
List<Map<String, Object>> getAllConversations();
|
||||
List<ConversationDto> getAllConversations();
|
||||
|
||||
/**
|
||||
* Delete a conversation
|
||||
*
|
||||
* @param conversationId Conversation ID to delete
|
||||
* @return true if deleted, false if conversation not found
|
||||
*/
|
||||
void deleteConversation(String conversationId);
|
||||
}
|
||||
boolean deleteConversation(String conversationId);
|
||||
|
||||
/**
|
||||
* Check if a conversation exists
|
||||
*
|
||||
* @param conversationId Conversation ID to check
|
||||
* @return true if conversation exists, false otherwise
|
||||
*/
|
||||
boolean conversationExists(String conversationId);
|
||||
}
|
||||
+28
@@ -18,11 +18,39 @@
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.hertzbeat.ai.agent.config.CustomSseServerTransport;
|
||||
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.web.servlet.function.RouterFunction;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
|
||||
/**
|
||||
* Service interface for MCP server operations.
|
||||
*/
|
||||
public interface McpServerService {
|
||||
|
||||
/**
|
||||
* Provides the HertzBeat tools for the MCP server
|
||||
* @return ToolCallbackProvider with all HertzBeat monitoring tools
|
||||
*/
|
||||
ToolCallbackProvider hertzbeatTools();
|
||||
|
||||
/**
|
||||
* Provides a custom SSE server transport for the MCP server
|
||||
* @param objectMapper the ObjectMapper instance for JSON serialization
|
||||
* @param serverProperties the properties for the MCP server configuration
|
||||
* @return a CustomSseServerTransport instance configured with the provided properties
|
||||
*/
|
||||
CustomSseServerTransport webMvcSseServerTransportProvider(
|
||||
ObjectMapper objectMapper,
|
||||
McpServerProperties serverProperties
|
||||
);
|
||||
|
||||
/**
|
||||
* Provides the MCP server router function for web MVC
|
||||
* @param transport Custom SSE server transport
|
||||
* @return RouterFunction for handling MCP server requests
|
||||
*/
|
||||
RouterFunction<ServerResponse> mvcMcpRouterFunction(CustomSseServerTransport transport);
|
||||
}
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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.ai.agent.service;
|
||||
|
||||
import org.apache.hertzbeat.ai.agent.event.OpenAiConfigChangeEvent;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
|
||||
|
||||
/**
|
||||
* OpenAI Configuration Service
|
||||
* Consolidated service for OpenAI configuration, validation, and client factory management
|
||||
*/
|
||||
public interface OpenAiConfigService {
|
||||
|
||||
/**
|
||||
* Save OpenAI configuration
|
||||
* @param config OpenAI configuration
|
||||
*/
|
||||
void saveConfig(OpenAiConfigDto config);
|
||||
|
||||
/**
|
||||
* Get OpenAI configuration
|
||||
* @return OpenAI configuration
|
||||
*/
|
||||
OpenAiConfigDto getConfig();
|
||||
|
||||
/**
|
||||
* Check if OpenAI is properly configured
|
||||
* @return true if configured and enabled
|
||||
*/
|
||||
boolean isConfigured();
|
||||
|
||||
/**
|
||||
* Get effective OpenAI configuration (DB first, then YAML fallback)
|
||||
* @return effective configuration or null if not configured
|
||||
*/
|
||||
OpenAiConfigDto getEffectiveConfig();
|
||||
|
||||
/**
|
||||
* Validate OpenAI API key by calling the OpenAI API
|
||||
* @param apiKey the API key to validate
|
||||
* @return validation result with success status and message
|
||||
*/
|
||||
ValidationResult validateApiKey(String apiKey);
|
||||
|
||||
/**
|
||||
* Force reload of OpenAI configuration cache
|
||||
* This method is typically called when configuration changes
|
||||
*/
|
||||
void reloadConfig();
|
||||
|
||||
/**
|
||||
* Handle OpenAI configuration change events
|
||||
* @param event OpenAI configuration change event
|
||||
*/
|
||||
void onOpenAiConfigChange(OpenAiConfigChangeEvent event);
|
||||
|
||||
/**
|
||||
* Validation result class
|
||||
*/
|
||||
class ValidationResult {
|
||||
private final boolean valid;
|
||||
private final String message;
|
||||
|
||||
private ValidationResult(boolean valid, String message) {
|
||||
this.valid = valid;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public static ValidationResult success(String message) {
|
||||
return new ValidationResult(true, message);
|
||||
}
|
||||
|
||||
public static ValidationResult failure(String message) {
|
||||
return new ValidationResult(false, message);
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return valid;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
-12
@@ -18,20 +18,30 @@
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.service.impl;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.config.PromptProvider;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.MessageDto;
|
||||
import org.apache.hertzbeat.ai.agent.service.ChatClientProviderService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Implementation of the {@link ChatClientProviderService}.
|
||||
* Provides functionality to interact with the ChatClient for handling chat
|
||||
* messages.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ChatClientProviderServiceImpl implements ChatClientProviderService {
|
||||
|
||||
@@ -42,11 +52,10 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
|
||||
private ToolCallbackProvider toolCallbackProvider;
|
||||
|
||||
@Autowired
|
||||
public ChatClientProviderServiceImpl(@Qualifier("openAiChatClient") ChatClient openAiChatClient) {
|
||||
public ChatClientProviderServiceImpl(ChatClient openAiChatClient) {
|
||||
this.chatClient = openAiChatClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String complete(String message) {
|
||||
return this.chatClient.prompt()
|
||||
.user(message)
|
||||
@@ -55,16 +64,37 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
|
||||
}
|
||||
|
||||
@Override
|
||||
public String streamChat(ChatRequestContext context) {
|
||||
public Flux<String> streamChat(ChatRequestContext context) {
|
||||
try {
|
||||
return this.chatClient.prompt(PromptProvider.HERTZBEAT_MONITORING_PROMPT)
|
||||
.user(context.getMessage())
|
||||
.toolCallbacks(toolCallbackProvider)
|
||||
.call()
|
||||
.content();
|
||||
} catch (Exception e) {
|
||||
return "Error: " + e.getMessage();
|
||||
}
|
||||
List<Message> messages = new ArrayList<>();
|
||||
|
||||
// Add conversation history if available
|
||||
if (context.getConversationHistory() != null && !context.getConversationHistory().isEmpty()) {
|
||||
for (MessageDto historyMessage : context.getConversationHistory()) {
|
||||
if ("user".equals(historyMessage.getRole())) {
|
||||
messages.add(new UserMessage(historyMessage.getContent()));
|
||||
} else if ("assistant".equals(historyMessage.getRole())) {
|
||||
messages.add(new AssistantMessage(historyMessage.getContent()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messages.add(new UserMessage(context.getMessage()));
|
||||
|
||||
log.info("Starting streaming chat for conversation: {}", context.getConversationId());
|
||||
|
||||
return this.chatClient.prompt()
|
||||
.messages(messages)
|
||||
.system(PromptProvider.HERTZBEAT_SYSTEM_PROMPT)
|
||||
.toolCallbacks(toolCallbackProvider)
|
||||
.stream()
|
||||
.content()
|
||||
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", context.getConversationId()))
|
||||
.doOnError(error -> log.error("Error in streaming chat: {}", error.getMessage(), error));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error setting up streaming chat: {}", e.getMessage(), e);
|
||||
return Flux.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+250
-3
@@ -15,15 +15,262 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.service.impl;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatResponseDto;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ConversationDto;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.MessageDto;
|
||||
import org.apache.hertzbeat.ai.agent.service.ChatClientProviderService;
|
||||
import org.apache.hertzbeat.ai.agent.service.ConversationService;
|
||||
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Implementation of the ConversationService interface for managing chat conversations.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ConversationServiceImpl {
|
||||
public class ConversationServiceImpl implements ConversationService {
|
||||
|
||||
}
|
||||
private final Map<String, Map<String, Object>> conversations = new ConcurrentHashMap<>();
|
||||
private final Map<String, List<Map<String, Object>>> conversationMessages = new ConcurrentHashMap<>();
|
||||
|
||||
@Autowired
|
||||
private ChatClientProviderService chatClientProviderService;
|
||||
|
||||
@Autowired
|
||||
private OpenAiConfigService openAiConfigService;
|
||||
|
||||
@Override
|
||||
public ConversationDto createConversation() {
|
||||
String conversationId = createNewConversation();
|
||||
return getConversation(conversationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<ServerSentEvent<ChatResponseDto>> streamChat(String message, String conversationId) {
|
||||
// Validate conversation exists
|
||||
if (!conversationExists(conversationId)) {
|
||||
ChatResponseDto errorResponse = ChatResponseDto.builder()
|
||||
.conversationId(conversationId)
|
||||
.response("Error: Conversation not found: " + conversationId)
|
||||
.build();
|
||||
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||
.event("error")
|
||||
.build());
|
||||
}
|
||||
|
||||
// Check if OpenAI is properly configured
|
||||
if (!openAiConfigService.isConfigured()) {
|
||||
ChatResponseDto errorResponse = ChatResponseDto.builder()
|
||||
.conversationId(conversationId)
|
||||
.response("OpenAI is not configured. Please configure your OpenAI API key in the settings or application.yml file.")
|
||||
.build();
|
||||
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||
.event("error")
|
||||
.build());
|
||||
}
|
||||
|
||||
log.info("Starting streaming conversation: {}", conversationId);
|
||||
|
||||
// Add user message to conversation
|
||||
String userMessageId = addMessageToConversation(conversationId, message, "user");
|
||||
|
||||
// Get conversation history for context
|
||||
List<Map<String, Object>> messagesList = conversationMessages.get(conversationId);
|
||||
List<MessageDto> conversationHistory = new ArrayList<>();
|
||||
|
||||
if (messagesList != null && messagesList.size() > 1) {
|
||||
// Get all messages except the last one (which is the current user message we just added)
|
||||
for (int i = 0; i < messagesList.size() - 1; i++) {
|
||||
Map<String, Object> msgMap = messagesList.get(i);
|
||||
conversationHistory.add(mapToMessageDto(msgMap));
|
||||
}
|
||||
}
|
||||
|
||||
ChatRequestContext context = ChatRequestContext.builder()
|
||||
.message(message)
|
||||
.conversationId(conversationId)
|
||||
.conversationHistory(conversationHistory)
|
||||
.build();
|
||||
|
||||
// Stream response from AI service
|
||||
StringBuilder fullResponse = new StringBuilder();
|
||||
return chatClientProviderService.streamChat(context)
|
||||
.map(chunk -> {
|
||||
fullResponse.append(chunk);
|
||||
ChatResponseDto responseDto = ChatResponseDto.builder()
|
||||
.conversationId(conversationId)
|
||||
.response(chunk)
|
||||
.userMessageId(userMessageId)
|
||||
.build();
|
||||
|
||||
return ServerSentEvent.builder(responseDto)
|
||||
.event("message")
|
||||
.build();
|
||||
})
|
||||
.concatWith(Flux.defer(() -> {
|
||||
// Add the complete AI response to conversation
|
||||
String assistantMessageId = addMessageToConversation(conversationId, fullResponse.toString(), "assistant");
|
||||
|
||||
ChatResponseDto finalResponse = ChatResponseDto.builder()
|
||||
.conversationId(conversationId)
|
||||
.response("")
|
||||
.userMessageId(userMessageId)
|
||||
.assistantMessageId(assistantMessageId)
|
||||
.build();
|
||||
|
||||
return Flux.just(ServerSentEvent.builder(finalResponse)
|
||||
.event("complete")
|
||||
.build());
|
||||
}))
|
||||
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", conversationId))
|
||||
.doOnError(error -> log.error("Error in streaming chat for conversation {}: {}", conversationId, error.getMessage(), error))
|
||||
.onErrorResume(error -> {
|
||||
ChatResponseDto errorResponse = ChatResponseDto.builder()
|
||||
.conversationId(conversationId)
|
||||
.response("An error occurred: " + error.getMessage())
|
||||
.userMessageId(userMessageId)
|
||||
.build();
|
||||
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||
.event("error")
|
||||
.build());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConversationDto getConversation(String conversationId) {
|
||||
if (conversationId == null || conversationId.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, Object> conversation = conversations.get(conversationId);
|
||||
if (conversation == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> messagesList = conversationMessages.get(conversationId);
|
||||
List<MessageDto> messages = messagesList != null
|
||||
? messagesList.stream().map(this::mapToMessageDto).collect(Collectors.toList()) :
|
||||
new ArrayList<>();
|
||||
|
||||
return ConversationDto.builder()
|
||||
.conversationId((String) conversation.get("conversationId"))
|
||||
.createdAt((LocalDateTime) conversation.get("createdAt"))
|
||||
.updatedAt((LocalDateTime) conversation.get("updatedAt"))
|
||||
.messages(messages)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ConversationDto> getAllConversations() {
|
||||
List<ConversationDto> result = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, Map<String, Object>> entry : conversations.entrySet()) {
|
||||
Map<String, Object> conv = entry.getValue();
|
||||
List<Map<String, Object>> messages = conversationMessages.get(entry.getKey());
|
||||
|
||||
ConversationDto dto = ConversationDto.builder()
|
||||
.conversationId((String) conv.get("conversationId"))
|
||||
.createdAt((LocalDateTime) conv.get("createdAt"))
|
||||
.updatedAt((LocalDateTime) conv.get("updatedAt"))
|
||||
.messages(new ArrayList<>()) // Don't include messages in list view for performance
|
||||
.build();
|
||||
result.add(dto);
|
||||
}
|
||||
|
||||
result.sort((a, b) -> b.getUpdatedAt().compareTo(a.getUpdatedAt()));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteConversation(String conversationId) {
|
||||
if (conversationId == null || conversationId.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean existed = conversations.containsKey(conversationId);
|
||||
if (existed) {
|
||||
conversations.remove(conversationId);
|
||||
conversationMessages.remove(conversationId);
|
||||
log.info("Deleted conversation: {}", conversationId);
|
||||
}
|
||||
return existed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean conversationExists(String conversationId) {
|
||||
return conversationId != null && !conversationId.isEmpty() && conversations.containsKey(conversationId);
|
||||
}
|
||||
|
||||
private String createNewConversation() {
|
||||
String conversationId = "conv-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
Map<String, Object> conversation = new HashMap<>();
|
||||
conversation.put("conversationId", conversationId);
|
||||
conversation.put("createdAt", now);
|
||||
conversation.put("updatedAt", now);
|
||||
|
||||
conversations.put(conversationId, conversation);
|
||||
conversationMessages.put(conversationId, new ArrayList<>());
|
||||
|
||||
log.info("Created new conversation: {}", conversationId);
|
||||
return conversationId;
|
||||
}
|
||||
|
||||
private MessageDto mapToMessageDto(Map<String, Object> messageMap) {
|
||||
return MessageDto.builder()
|
||||
.messageId((String) messageMap.get("messageId"))
|
||||
.conversationId((String) messageMap.get("conversationId"))
|
||||
.content((String) messageMap.get("content"))
|
||||
.role((String) messageMap.get("role"))
|
||||
.timestamp((LocalDateTime) messageMap.get("timestamp"))
|
||||
.build();
|
||||
}
|
||||
|
||||
private String addMessageToConversation(String conversationId, String content, String role) {
|
||||
List<Map<String, Object>> messages = conversationMessages.computeIfAbsent(conversationId, k -> new ArrayList<>());
|
||||
|
||||
String messageId = "msg-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("messageId", messageId);
|
||||
message.put("conversationId", conversationId);
|
||||
message.put("content", content);
|
||||
message.put("role", role);
|
||||
message.put("timestamp", LocalDateTime.now());
|
||||
|
||||
messages.add(message);
|
||||
|
||||
// Update conversation timestamp
|
||||
Map<String, Object> conversation = conversations.get(conversationId);
|
||||
if (conversation != null) {
|
||||
conversation.put("updatedAt", LocalDateTime.now());
|
||||
// Auto-generate title from first user message
|
||||
if ("user".equals(role) && messages.stream().filter(m -> "user".equals(m.get("role"))).count() == 1) {
|
||||
String title = content.length() > 30 ? content.substring(0, 27) + "..." : content;
|
||||
conversation.put("title", title);
|
||||
}
|
||||
}
|
||||
|
||||
return messageId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
+13
-4
@@ -20,15 +20,18 @@ package org.apache.hertzbeat.ai.agent.service.impl;
|
||||
|
||||
import org.apache.hertzbeat.ai.agent.config.CustomSseServerTransport;
|
||||
import org.apache.hertzbeat.ai.agent.service.McpServerService;
|
||||
import org.apache.hertzbeat.ai.agent.tools.AlertDefineTools;
|
||||
import org.apache.hertzbeat.ai.agent.tools.AlertTools;
|
||||
import org.apache.hertzbeat.ai.agent.tools.MetricsTools;
|
||||
import org.apache.hertzbeat.ai.agent.tools.MonitorTools;
|
||||
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.hertzbeat.ai.agent.tools.impl.MonitorToolsImpl;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.web.servlet.function.RouterFunction;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
@@ -41,11 +44,17 @@ import org.springframework.web.servlet.function.ServerResponse;
|
||||
@Configuration
|
||||
public class McpServerServiceImpl implements McpServerService {
|
||||
@Autowired
|
||||
private MonitorToolsImpl monitorTools;
|
||||
private MonitorTools monitorTools;
|
||||
@Autowired
|
||||
private AlertTools alertTools;
|
||||
@Autowired
|
||||
private MetricsTools metricsTools;
|
||||
@Autowired
|
||||
private AlertDefineTools alertDefineTools;
|
||||
|
||||
@Bean
|
||||
public ToolCallbackProvider hertzbeatTools() {
|
||||
return MethodToolCallbackProvider.builder().toolObjects(monitorTools).build();
|
||||
return MethodToolCallbackProvider.builder().toolObjects(monitorTools, alertTools, alertDefineTools, metricsTools).build();
|
||||
}
|
||||
/**
|
||||
* Provides a custom SSE server transport for the MCP server.
|
||||
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* 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.ai.agent.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.config.OpenAiYamlConfig;
|
||||
import org.apache.hertzbeat.ai.agent.dao.OpenAiConfigDao;
|
||||
import org.apache.hertzbeat.ai.agent.entity.OpenAiConfig;
|
||||
import org.apache.hertzbeat.ai.agent.event.OpenAiConfigChangeEvent;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
|
||||
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* OpenAI Configuration Service Implementation
|
||||
* Consolidated service for OpenAI configuration, validation, and client factory management
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class OpenAiConfigServiceImpl implements OpenAiConfigService {
|
||||
|
||||
private static final String CONFIG_TYPE = "openai";
|
||||
private static final String OPENAI_MODELS_ENDPOINT = "https://api.openai.com/v1/models";
|
||||
|
||||
private final OpenAiConfigDao openAiConfigDao;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ApplicationContext applicationContext;
|
||||
private final OpenAiYamlConfig yamlConfig;
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
// Client factory cache
|
||||
private volatile OpenAiConfigDto currentConfig;
|
||||
|
||||
public OpenAiConfigServiceImpl(OpenAiConfigDao openAiConfigDao,
|
||||
ObjectMapper objectMapper,
|
||||
ApplicationContext applicationContext,
|
||||
OpenAiYamlConfig yamlConfig) {
|
||||
this.openAiConfigDao = openAiConfigDao;
|
||||
this.objectMapper = objectMapper;
|
||||
this.applicationContext = applicationContext;
|
||||
this.yamlConfig = yamlConfig;
|
||||
this.restTemplate = new RestTemplate();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveConfig(OpenAiConfigDto config) {
|
||||
try {
|
||||
String contentJson = objectMapper.writeValueAsString(config);
|
||||
|
||||
OpenAiConfig openAiConfig = OpenAiConfig.builder()
|
||||
.type(CONFIG_TYPE)
|
||||
.content(contentJson)
|
||||
.build();
|
||||
|
||||
openAiConfigDao.save(openAiConfig);
|
||||
log.info("OpenAI configuration saved successfully");
|
||||
|
||||
applicationContext.publishEvent(new OpenAiConfigChangeEvent(applicationContext));
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException("Failed to save OpenAI configuration: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenAiConfigDto getConfig() {
|
||||
OpenAiConfig config = openAiConfigDao.findByType(CONFIG_TYPE);
|
||||
if (config == null || !StringUtils.hasText(config.getContent())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return objectMapper.readValue(config.getContent(), OpenAiConfigDto.class);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException("Failed to parse OpenAI configuration: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigured() {
|
||||
OpenAiConfigDto effective = getEffectiveConfig();
|
||||
return effective != null && effective.isEnable() && StringUtils.hasText(effective.getApiKey());
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenAiConfigDto getEffectiveConfig() {
|
||||
OpenAiConfigDto dbConfig = getConfig();
|
||||
if (dbConfig != null && dbConfig.isEnable() && StringUtils.hasText(dbConfig.getApiKey())) {
|
||||
log.debug("Using database OpenAI configuration");
|
||||
return dbConfig;
|
||||
}
|
||||
|
||||
if (yamlConfig != null && yamlConfig.isEnable() && StringUtils.hasText(yamlConfig.getApiKey())) {
|
||||
log.debug("Using YAML OpenAI configuration from spring.ai.openai.api-key");
|
||||
OpenAiConfigDto yamlDto = new OpenAiConfigDto();
|
||||
yamlDto.setEnable(true);
|
||||
yamlDto.setApiKey(yamlConfig.getApiKey());
|
||||
return yamlDto;
|
||||
}
|
||||
|
||||
log.debug("No valid OpenAI configuration found");
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidationResult validateApiKey(String apiKey) {
|
||||
if (!StringUtils.hasText(apiKey)) {
|
||||
return ValidationResult.failure("API key cannot be empty");
|
||||
}
|
||||
|
||||
if (!apiKey.startsWith("sk-")) {
|
||||
return ValidationResult.failure("Invalid API key format. OpenAI API keys should start with 'sk-'");
|
||||
}
|
||||
|
||||
try {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Authorization", "Bearer " + apiKey);
|
||||
headers.set("Content-Type", "application/json");
|
||||
|
||||
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||
|
||||
log.debug("Validating OpenAI API key by calling models endpoint");
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
OPENAI_MODELS_ENDPOINT,
|
||||
HttpMethod.GET,
|
||||
entity,
|
||||
String.class
|
||||
);
|
||||
|
||||
if (response.getStatusCode() == HttpStatus.OK) {
|
||||
log.info("OpenAI API key validation successful");
|
||||
return ValidationResult.success("API key is valid");
|
||||
} else {
|
||||
log.warn("OpenAI API key validation failed with status: {}", response.getStatusCode());
|
||||
return ValidationResult.failure("API key validation failed: " + response.getStatusCode());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error validating OpenAI API key", e);
|
||||
String errorMessage = e.getMessage();
|
||||
|
||||
// Parse common error messages
|
||||
if (errorMessage.contains("401")) {
|
||||
return ValidationResult.failure("Invalid API key - authentication failed");
|
||||
} else if (errorMessage.contains("403")) {
|
||||
return ValidationResult.failure("API key does not have permission to access models");
|
||||
} else if (errorMessage.contains("429")) {
|
||||
return ValidationResult.failure("Rate limit exceeded - please try again later");
|
||||
} else if (errorMessage.contains("timeout") || errorMessage.contains("connect")) {
|
||||
return ValidationResult.failure("Network error - unable to connect to OpenAI API");
|
||||
} else {
|
||||
return ValidationResult.failure("API key validation failed: " + errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reloadConfig() {
|
||||
synchronized (this) {
|
||||
currentConfig = null; // Force reload
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI configuration change event listener
|
||||
*/
|
||||
@EventListener(OpenAiConfigChangeEvent.class)
|
||||
public void onOpenAiConfigChange(OpenAiConfigChangeEvent event) {
|
||||
log.info("[OpenAiConfigService] OpenAI configuration change event received");
|
||||
reloadConfig();
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.ai.agent.tools;
|
||||
|
||||
/**
|
||||
* Tools for alert definition and threshold configuration operations
|
||||
*/
|
||||
public interface AlertDefineTools {
|
||||
|
||||
/**
|
||||
* Create a new alert rule with HertzBeat's expression format based on app hierarchy
|
||||
|
||||
*
|
||||
* @param name Alert rule name (required, must be unique)
|
||||
* @param app App name from hierarchy (must match exact hierarchy app value)
|
||||
* @param metrics Metrics name from hierarchy (must match exact hierarchy metrics value)
|
||||
* @param fieldConditions Field-specific conditions from metric's children (e.g., "VmName = 'arora'", "total_granted > 1000",
|
||||
* "total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)")
|
||||
* @param type Alert rule type: 'realtime' (default) or 'periodic'
|
||||
* @param period Execution period in seconds (only for periodic rules, default: 300)
|
||||
* @param times Number of consecutive violations before triggering (default: 3)
|
||||
* @param priority Alert priority as integer: 0=critical, 1=warning, 2=info (default: 1)
|
||||
* @param description Alert rule description (optional)
|
||||
* @param template Alert message template with variables (optional)
|
||||
* @param datasource Data source type: 'promql' (default)
|
||||
* @param labels Labels as key:value pairs separated by commas
|
||||
* @param annotations Annotations as key:value pairs separated by commas
|
||||
* @param enable Whether to enable the rule immediately (default: true)
|
||||
* @return Result message with rule ID if successful
|
||||
*/
|
||||
String createAlertRule(String name, String app, String metrics, String fieldConditions,
|
||||
String type, Integer period, Integer times, Integer priority, String description,
|
||||
String template, String datasource, String labels, String annotations, Boolean enable);
|
||||
|
||||
/**
|
||||
* List existing alert rules with filtering
|
||||
* @param search Search term for rule name or description
|
||||
* @param monitorType Filter by monitor type
|
||||
* @param enabled Filter by enabled status
|
||||
* @param pageIndex Page index
|
||||
* @param pageSize Page size
|
||||
* @return Formatted list of alert rules
|
||||
*/
|
||||
String listAlertRules(String search, String monitorType, Boolean enabled, Integer pageIndex, Integer pageSize);
|
||||
|
||||
/**
|
||||
* Enable or disable an alert rule
|
||||
* @param ruleId Alert rule ID
|
||||
* @param enabled Whether to enable the rule
|
||||
* @return Result message
|
||||
*/
|
||||
String toggleAlertRule(Long ruleId, Boolean enabled);
|
||||
|
||||
/**
|
||||
* Get detailed information about an alert rule
|
||||
* @param ruleId Alert rule ID
|
||||
* @return Detailed rule information
|
||||
*/
|
||||
String getAlertRuleDetails(Long ruleId);
|
||||
|
||||
|
||||
/**
|
||||
* Get the hierarchical structure of available apps and metrics for alert rule creation
|
||||
* @param app App type to get hierarchy for (optional, gets all if not specified)
|
||||
* @return Hierarchical structure showing apps and their available metrics
|
||||
*/
|
||||
String getAppsMetricsHierarchy(String app);
|
||||
|
||||
/**
|
||||
* Bind monitors to an alert rule by modifying the alert expression
|
||||
* @param ruleId Alert rule ID to bind monitors to
|
||||
* @param monitorIds Comma-separated list of monitor IDs to bind
|
||||
* @return Result message indicating success or failure
|
||||
*/
|
||||
String bindMonitorsToAlertRule(Long ruleId, String monitorIds);
|
||||
}
|
||||
+22
-1
@@ -19,7 +19,28 @@
|
||||
package org.apache.hertzbeat.ai.agent.tools;
|
||||
|
||||
/**
|
||||
* Tools for alert operations
|
||||
* Tools for alert operations and alarm data queries
|
||||
*/
|
||||
public interface AlertTools {
|
||||
|
||||
/**
|
||||
* Query alerts with comprehensive filtering and pagination
|
||||
* @param alertType Alert type (single, group, both)
|
||||
* @param status Alert status (firing, resolved, all)
|
||||
* @param search Search term for alert content or labels
|
||||
* @param sort Sort field (startAt, triggerTimes, status)
|
||||
* @param order Sort order (asc, desc)
|
||||
* @param pageIndex Page index
|
||||
* @param pageSize Page size
|
||||
* @return Formatted string with alert information
|
||||
*/
|
||||
String queryAlerts(String alertType, String status, String search, String sort, String order, Integer pageIndex, Integer pageSize);
|
||||
|
||||
/**
|
||||
* Get alerts summary statistics
|
||||
* @return Alert summary information including counts by status
|
||||
*/
|
||||
String getAlertsSummary();
|
||||
|
||||
|
||||
}
|
||||
|
||||
+29
-1
@@ -19,7 +19,35 @@
|
||||
package org.apache.hertzbeat.ai.agent.tools;
|
||||
|
||||
/**
|
||||
* Tools for metrics operations
|
||||
* Tools for metrics data operations and queries
|
||||
*/
|
||||
public interface MetricsTools {
|
||||
|
||||
/**
|
||||
* Get real-time metrics data for a monitor
|
||||
* @param monitorId Monitor ID
|
||||
* @param metrics Metrics name (e.g., "system", "cpu", "memory")
|
||||
* @return Formatted real-time metrics data
|
||||
*/
|
||||
String getRealtimeMetrics(Long monitorId, String metrics);
|
||||
|
||||
/**
|
||||
* Get historical metrics data for a monitor
|
||||
* @param monitorId Monitor ID
|
||||
* @param app Monitor type (e.g., "linux", "mysql", "http")
|
||||
* @param metrics Metrics name (e.g., "system", "cpu", "memory")
|
||||
* @param metric Specific metric field (e.g., "usage", "used", "available")
|
||||
* @param label Label filter for specific instances
|
||||
* @param history Time range (e.g., "1h", "6h", "24h", "7d")
|
||||
* @param interval Whether to aggregate data with intervals
|
||||
* @return Historical metrics data formatted for display
|
||||
*/
|
||||
String getHistoricalMetrics(Long monitorId, String app, String metrics, String metric, String label, String history, Boolean interval);
|
||||
|
||||
/**
|
||||
* Check warehouse storage server status
|
||||
* @return Status of the metrics storage system
|
||||
*/
|
||||
String getWarehouseStatus();
|
||||
|
||||
}
|
||||
|
||||
+57
-10
@@ -18,8 +18,6 @@
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.tools;
|
||||
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -27,15 +25,57 @@ import java.util.List;
|
||||
*/
|
||||
public interface MonitorTools {
|
||||
|
||||
String addMonitor(String name, ToolContext context);
|
||||
/**
|
||||
* Add a new monitor with comprehensive configuration
|
||||
*
|
||||
* @param name Monitor name
|
||||
* @param app Monitor type/application (e.g., 'linux', 'mysql', 'http')
|
||||
* @param host Target host (IP address or domain name)
|
||||
* @param port Target port (optional, depends on monitor type)
|
||||
* @param intervals Collection interval in seconds (default: 600)
|
||||
* @param username Username for authentication (optional)
|
||||
* @param password Password for authentication (optional)
|
||||
* @param database Database name (for database monitors)
|
||||
* @param additionalParams Additional app-specific parameters as JSON string (optional)
|
||||
* @param description Monitor description (optional)
|
||||
* @return Result message with monitor ID if successful
|
||||
*/
|
||||
String addMonitor(
|
||||
String name,
|
||||
String app,
|
||||
String host,
|
||||
Integer port,
|
||||
Integer intervals,
|
||||
String username,
|
||||
String password,
|
||||
String database,
|
||||
String additionalParams,
|
||||
String description
|
||||
);
|
||||
|
||||
/**
|
||||
* List all available monitor types that can be added
|
||||
*
|
||||
* @param language Language code for localized names (e.g., 'en-US', 'zh-CN')
|
||||
* @return Formatted string list of available monitor types with descriptions
|
||||
*/
|
||||
String listMonitorTypes(String language);
|
||||
|
||||
/**
|
||||
* Query monitor information with flexible filtering and pagination.
|
||||
* Supports filtering by monitor IDs, type, status, host, labels, sorting, and
|
||||
* pagination.
|
||||
* Returns results as plain JSON.
|
||||
* Comprehensive monitor querying with flexible filtering, pagination, and specialized views
|
||||
* @param ids Specific monitor IDs to retrieve (optional)
|
||||
* @param app Monitor type filter (linux, mysql, http, etc.)
|
||||
* @param status Monitor status (1=online, 2=offline, 3=unreachable, 0=paused, 9=all)
|
||||
* @param search Search in monitor names or hosts (partial matching)
|
||||
* @param labels Label filters, format: 'key1:value1,key2:value2'
|
||||
* @param sort Sort field (name, gmtCreate, gmtUpdate, status, app)
|
||||
* @param order Sort order (asc, desc)
|
||||
* @param pageIndex Page number starting from 0
|
||||
* @param pageSize Items per page (1-100 recommended)
|
||||
* @param includeStats Include status statistics summary
|
||||
* @return Comprehensive monitor information with optional statistics
|
||||
*/
|
||||
String listMonitors(
|
||||
String queryMonitors(
|
||||
List<Long> ids,
|
||||
String app,
|
||||
Byte status,
|
||||
@@ -45,6 +85,13 @@ public interface MonitorTools {
|
||||
String order,
|
||||
Integer pageIndex,
|
||||
Integer pageSize,
|
||||
ToolContext context);
|
||||
Boolean includeStats);
|
||||
|
||||
}
|
||||
/**
|
||||
* Get parameter definitions required for a specific monitor type
|
||||
*
|
||||
* @param app Monitor type/application name (e.g., 'linux', 'mysql', 'redis')
|
||||
* @return Formatted string with parameter definitions including field names, types, and requirements
|
||||
*/
|
||||
String getMonitorAdditionalParams(String app);
|
||||
}
|
||||
+665
@@ -0,0 +1,665 @@
|
||||
/*
|
||||
* 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.ai.agent.tools.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.adapters.AlertDefineServiceAdapter;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.Hierarchy;
|
||||
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
|
||||
import org.apache.hertzbeat.ai.agent.tools.AlertDefineTools;
|
||||
import org.apache.hertzbeat.ai.agent.utils.UtilityClass;
|
||||
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Implementation of Alert Define Tools functionality
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
@Autowired
|
||||
private AlertDefineServiceAdapter alertDefineServiceAdapter;
|
||||
|
||||
|
||||
@Override
|
||||
@Tool(name = "create_alert_rule", description = """
|
||||
ALERT RULE means when to alert a user
|
||||
THESE ARE ALERT RULES WITH THRESHOLD VALUES. USERS CAN SPECIFY THE THRESHOLD VALUES FOR EXAMPLE,
|
||||
IF THE USER SAYS "ALERT ME WHEN MY COST EXCEEDS 700, THE EXPRESSION SHOULD BE 'cost > 700' NOT 'cost < 700'.
|
||||
APPLY THE SAME LOGIC FOR LESS THAN OPERATOR.
|
||||
Create a HertzBeat alert rule based on app hierarchy structure and user requirements.
|
||||
It is important to first understand the hierarchy of apps, metrics, and field conditions
|
||||
Each app has its own metrics and each metric has its own field conditions.
|
||||
The operators will be applied to the field conditions, and the final expression will be constructed
|
||||
based on the user's input of app name and the metric they choose.
|
||||
CRITICAL WORKFLOW Do all of this iteratively with user interaction at each step
|
||||
1. ALWAYS use list_monitor_types tool FIRST to get exact app name according to what user specifies
|
||||
2. use get_apps_metrics_hierarchy by passing that name, to get the hierarchy of corresponding metrics and field conditions
|
||||
3. Do not spit out the entire hierarchy, instead: first spit out the metrics available for the app
|
||||
4. Ask the user to choose a metric from the available metrics
|
||||
5. Based on the metric chosen, present the available field conditions/params
|
||||
6. You will construct the proper expression with field conditions
|
||||
7. once this tool successfully executes, ask the user if they want to bind any existing monitors to this alert rule,
|
||||
get the monitors list for a particular app using the query_monitors tool.
|
||||
8. based on the user's output, conditionally call the bind_monitors_to_alert_rule tool to bind monitors to the alert rule
|
||||
VERY VERY IMPORTANT:
|
||||
- ALWAYS USE the value field from the get_apps_metrics_hierarchy's json response when creating alert expressions on the field parameters
|
||||
|
||||
EXAMPLES FOR FIELD CONDITION EXPRESSION( Do not copy these examples, they are just for reference ):
|
||||
These are all just examples, you can take inspiration from them and create a rule based on hierarchy, always ask the user for all params, do not assume them, even for these examples:
|
||||
|
||||
1. Kafka JVM Alert:
|
||||
- App: "kafka", Metric: "jvm_basic"
|
||||
- Field condition: equals(VmName, "myVM")
|
||||
- Field condition expression: equals(VmName, "myVM")
|
||||
|
||||
2. LLM Credits Alert:
|
||||
- App: "openai", Metric: "credit_grants"
|
||||
- Field condition: total_granted > some_value
|
||||
- Field condition expression: total_granted > 1000
|
||||
|
||||
3. HBase Master Alert:
|
||||
- App: "hbase_master", Metric: "server"
|
||||
- Field condition: heap_memory_used > 80 or some_factor<100
|
||||
- Field condition expression: heap_memory_used > 80 or some_factor<100
|
||||
|
||||
4. Complex OpenAI Credits Alert:
|
||||
- App: "openai", Metric: "credit_grants"
|
||||
- Field condition: total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
|
||||
- Field condition expression: total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
|
||||
|
||||
FIELD CONDITIONS GUIDANCE:
|
||||
- Field names come from metric's children in hierarchy (leaf nodes)
|
||||
- Use the "value" field from the metric's children, not the label when creating conditions
|
||||
- Supported operators: >, <, >=, <=, ==, !=, exists(), !exists() for numeric fields
|
||||
- equals(), contains(), matches(),exists(), !equals(), !contains(), !matches(), !exists() for string fields
|
||||
- Supported logical operators: and, or to connect different field parameter rules or rulesets
|
||||
- ONLY USE THESE OPERATORS, when creating conditions, do not use any other operators
|
||||
- Support grouping with parentheses: (condition1 and condition2) or condition3
|
||||
- String values should be quoted: equals(VmName, "my-vm")
|
||||
- Simple conditions: heap_memory_used > 80, total_granted <= 1000
|
||||
- Complex conditions: total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
|
||||
|
||||
PRIORITY LEVELS:
|
||||
- 0: Critical (immediate action required)
|
||||
- 1: Warning (attention needed, default)
|
||||
- 2: Info (informational only)
|
||||
""")
|
||||
public String createAlertRule(
|
||||
@ToolParam(description = "Alert rule name (required, must be unique)", required = true) String name,
|
||||
@ToolParam(description = "App name from hierarchy (must match exact hierarchy app value)", required = true) String app,
|
||||
@ToolParam(description = "Metrics name from hierarchy (must match exact hierarchy metrics value)", required = true) String metrics,
|
||||
@ToolParam(description = "Field conditions expression)", required = true) String fieldConditions,
|
||||
@ToolParam(description = "Alert rule type: 'realtime' (default) or 'periodic'", required = false) String type,
|
||||
@ToolParam(description = "Execution period in seconds (only for periodic rules, default: 300)", required = false) Integer period,
|
||||
@ToolParam(description = "Number of consecutive violations before triggering (default: 3)", required = false) Integer times,
|
||||
@ToolParam(description = "Alert priority as integer: 0=critical, 1=warning, 2=info (default: 1)", required = false) Integer priority,
|
||||
@ToolParam(description = "Alert rule description (optional)", required = false) String description,
|
||||
@ToolParam(description = "Alert message template with variables (optional)", required = false) String template,
|
||||
@ToolParam(description = "Data source type: 'promql' (default)", required = false) String datasource,
|
||||
@ToolParam(description = "Labels as key:value pairs separated by commas (e.g., 'env:prod,severity:critical')", required = false) String labels,
|
||||
@ToolParam(description = "Annotations as key:value pairs separated by commas (e.g., 'summary:High CPU')", required = false) String annotations,
|
||||
@ToolParam(description = "Whether to enable the rule immediately (default: true)", required = false) Boolean enable) {
|
||||
|
||||
try {
|
||||
log.info("Creating HertzBeat alert rule: name={}, app={}, metrics={}, fieldConditions={}", name, app, metrics, fieldConditions);
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current subject in create_alert_rule tool: {}", subjectSum);
|
||||
|
||||
// Validate required parameters
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
return "Error: Alert rule name is required";
|
||||
}
|
||||
if (app == null || app.trim().isEmpty()) {
|
||||
return "Error: App name is required (use get_apps_metrics_hierarchy to find exact names)";
|
||||
}
|
||||
if (metrics == null || metrics.trim().isEmpty()) {
|
||||
return "Error: Metrics name is required (use get_apps_metrics_hierarchy to find exact names)";
|
||||
}
|
||||
if (fieldConditions == null || fieldConditions.trim().isEmpty()) {
|
||||
return "Error: Field conditions are required (e.g., 'equals(VmName, \"arora\")', 'total_granted > 1000')";
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if (type == null || type.trim().isEmpty()) {
|
||||
type = "realtime";
|
||||
}
|
||||
if (times == null || times <= 0) {
|
||||
times = 3;
|
||||
}
|
||||
if (priority == null) {
|
||||
priority = 1; // Default to warning
|
||||
}
|
||||
if (enable == null) {
|
||||
enable = true;
|
||||
}
|
||||
if (datasource == null || datasource.trim().isEmpty()) {
|
||||
datasource = "promql";
|
||||
}
|
||||
|
||||
// Validate alert type
|
||||
if (!type.equals("realtime") && !type.equals("periodic")) {
|
||||
return "Error: Alert type must be 'realtime' or 'periodic'";
|
||||
}
|
||||
|
||||
// Validate priority
|
||||
if (priority < 0 || priority > 2) {
|
||||
return "Error: Priority must be 0 (critical), 1 (warning), or 2 (info)";
|
||||
}
|
||||
|
||||
// For periodic rules, validate period parameter
|
||||
if (type.equals("periodic")) {
|
||||
if (period == null || period <= 0) {
|
||||
period = 300; // Default 5 minutes
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL VALIDATION: Verify app-metric-field relationships using hierarchy
|
||||
String validationResult = validateHierarchyRelationships(app.trim(), metrics.trim(), fieldConditions.trim());
|
||||
if (!validationResult.equals("VALID")) {
|
||||
return validationResult; // Return validation error message
|
||||
}
|
||||
|
||||
// EXPRESSION VALIDATION: Verify field conditions syntax and operators
|
||||
String expressionValidation = UtilityClass.validateExpressionSyntax(fieldConditions.trim());
|
||||
if (!expressionValidation.equals("VALID")) {
|
||||
return expressionValidation; // Return expression validation error message
|
||||
}
|
||||
|
||||
String expr = String.format("equals(__app__,\"%s\") && equals(__metrics__,\"%s\") && %s",
|
||||
app.trim(), metrics.trim(), fieldConditions.trim());
|
||||
|
||||
|
||||
|
||||
// Parse labels if provided
|
||||
Map<String, String> labelsMap = new HashMap<>();
|
||||
if (labels != null && !labels.trim().isEmpty()) {
|
||||
labelsMap.putAll(UtilityClass.parseKeyValuePairs(labels));
|
||||
}
|
||||
// Add severity based on priority
|
||||
String severityLabel = priority == 0 ? "critical" : (priority == 1 ? "warning" : "info");
|
||||
labelsMap.put("severity", severityLabel);
|
||||
|
||||
// Parse annotations if provided
|
||||
Map<String, String> annotationsMap = new HashMap<>();
|
||||
if (annotations != null && !annotations.trim().isEmpty()) {
|
||||
annotationsMap.putAll(UtilityClass.parseKeyValuePairs(annotations));
|
||||
}
|
||||
// Add default annotations if not provided
|
||||
if (!annotationsMap.containsKey("summary")) {
|
||||
annotationsMap.put("summary", description != null ? description :
|
||||
String.format("Alert for %s %s when %s", app, metrics, fieldConditions));
|
||||
}
|
||||
if (!annotationsMap.containsKey("description")) {
|
||||
annotationsMap.put("description", String.format("Monitor %s metrics %s with conditions: %s", app, metrics, fieldConditions));
|
||||
}
|
||||
|
||||
// Generate default template if not provided
|
||||
if (template == null || template.trim().isEmpty()) {
|
||||
template = String.format("Alert: %s %s - %s", app, metrics, fieldConditions);
|
||||
}
|
||||
|
||||
// Create comprehensive alert definition
|
||||
AlertDefine alertDefine = AlertDefine.builder()
|
||||
.name(name.trim())
|
||||
.type(type)
|
||||
.expr(expr)
|
||||
.period(period)
|
||||
.times(times)
|
||||
.labels(labelsMap)
|
||||
.annotations(annotationsMap)
|
||||
.template(template)
|
||||
.datasource(datasource)
|
||||
.enable(enable)
|
||||
.build();
|
||||
|
||||
AlertDefine createdAlertDefine = alertDefineServiceAdapter.addAlertDefine(alertDefine);
|
||||
|
||||
// Note: Monitor binding is handled separately via bind_monitors_to_alert_rule tool
|
||||
String bindingNote = String.format(" (Use bind_monitors_to_alert_rule tool to associate specific monitors)");
|
||||
|
||||
log.info("Successfully created alert rule '{}' with ID: {}", name, createdAlertDefine.getId());
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append(String.format("Successfully created %s alert rule '%s' with ID: %d\n",
|
||||
type, name, createdAlertDefine.getId()));
|
||||
response.append(String.format("Expression: %s\n", expr));
|
||||
response.append(String.format("Priority: %d (%s)\n", priority, severityLabel));
|
||||
response.append(String.format("Trigger after: %d consecutive violations\n", times));
|
||||
if (type.equals("periodic")) {
|
||||
response.append(String.format("Execution period: %d seconds\n", period));
|
||||
}
|
||||
response.append(String.format("Data source: %s\n", datasource));
|
||||
response.append(String.format("Enabled: %s\n", enable));
|
||||
if (!labelsMap.isEmpty()) {
|
||||
response.append(String.format("Labels: %s\n", labelsMap));
|
||||
}
|
||||
response.append(bindingNote);
|
||||
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to create alert rule '{}': {}", name, e.getMessage(), e);
|
||||
return "Error creating alert rule '" + name + "': " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// ... other existing methods would go here ...
|
||||
|
||||
@Override
|
||||
@Tool(name = "list_alert_rules", description = """
|
||||
List existing alert rules with filtering options.
|
||||
Shows configured thresholds and alert definitions.
|
||||
""")
|
||||
public String listAlertRules(
|
||||
@ToolParam(description = "Search term for rule name or description", required = false) String search,
|
||||
@ToolParam(description = "Filter by monitor type", required = false) String monitorType,
|
||||
@ToolParam(description = "Filter by enabled status", required = false) Boolean enabled,
|
||||
@ToolParam(description = "Page index (default: 0)", required = false) Integer pageIndex,
|
||||
@ToolParam(description = "Page size (default: 10)", required = false) Integer pageSize) {
|
||||
|
||||
try {
|
||||
log.info("Listing alert rules: search={}, monitorType={}, enabled={}", search, monitorType, enabled);
|
||||
|
||||
if (pageIndex == null || pageIndex < 0) {
|
||||
pageIndex = 0;
|
||||
}
|
||||
if (pageSize == null || pageSize <= 0) {
|
||||
pageSize = 10;
|
||||
}
|
||||
|
||||
Page<AlertDefine> result = alertDefineServiceAdapter.getAlertDefines(
|
||||
search, monitorType, enabled, "gmtCreate", "desc", pageIndex, pageSize);
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("Found ").append(result.getContent().size())
|
||||
.append(" alert rules (Total: ").append(result.getTotalElements()).append("):\n\n");
|
||||
|
||||
for (AlertDefine alertDefine : result.getContent()) {
|
||||
response.append("Rule ID: ").append(alertDefine.getId()).append("\n");
|
||||
response.append("Name: ").append(alertDefine.getName()).append("\n");
|
||||
response.append("Expression: ").append(alertDefine.getExpr()).append("\n");
|
||||
response.append("Type: ").append(alertDefine.getType()).append("\n");
|
||||
response.append("Trigger Times: ").append(alertDefine.getTimes()).append("\n");
|
||||
response.append("Enabled: ").append(alertDefine.isEnable()).append("\n");
|
||||
|
||||
if (alertDefine.getLabels() != null && !alertDefine.getLabels().isEmpty()) {
|
||||
response.append("Labels: ").append(alertDefine.getLabels()).append("\n");
|
||||
}
|
||||
if (alertDefine.getAnnotations() != null && !alertDefine.getAnnotations().isEmpty()) {
|
||||
response.append("Summary: ").append(alertDefine.getAnnotations().get("summary")).append("\n");
|
||||
}
|
||||
response.append("Created: ").append(alertDefine.getGmtCreate()).append("\n");
|
||||
response.append("\n");
|
||||
}
|
||||
|
||||
if (result.getContent().isEmpty()) {
|
||||
response.append("No alert rules found matching the specified criteria.");
|
||||
}
|
||||
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to list alert rules: {}", e.getMessage(), e);
|
||||
return "Error retrieving alert rules: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "toggle_alert_rule", description = """
|
||||
Enable or disable an alert rule.
|
||||
Allows activating or deactivating threshold monitoring.
|
||||
""")
|
||||
public String toggleAlertRule(
|
||||
@ToolParam(description = "Alert rule ID", required = true) Long ruleId,
|
||||
@ToolParam(description = "Whether to enable the rule", required = true) Boolean enabled) {
|
||||
|
||||
try {
|
||||
log.info("Toggling alert rule ID: {} to enabled: {}", ruleId, enabled);
|
||||
|
||||
alertDefineServiceAdapter.toggleAlertDefineStatus(ruleId, enabled);
|
||||
|
||||
log.info("Successfully toggled alert rule ID: {} to enabled: {}", ruleId, enabled);
|
||||
return String.format("Successfully %s alert rule ID: %d",
|
||||
enabled ? "enabled" : "disabled", ruleId);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to toggle alert rule ID {}: {}", ruleId, e.getMessage(), e);
|
||||
return "Error toggling alert rule: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Tool(name = "get_alert_rule_details", description = """
|
||||
Get detailed information about a specific alert rule.
|
||||
Shows complete threshold configuration and rule settings.
|
||||
""")
|
||||
public String getAlertRuleDetails(
|
||||
@ToolParam(description = "Alert rule ID", required = true) Long ruleId) {
|
||||
|
||||
try {
|
||||
log.info("Getting alert rule details for ID: {}", ruleId);
|
||||
|
||||
AlertDefine alertDefine = alertDefineServiceAdapter.getAlertDefine(ruleId);
|
||||
if (alertDefine == null) {
|
||||
return "Alert rule with ID " + ruleId + " not found";
|
||||
}
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("ALERT RULE DETAILS\n");
|
||||
response.append("==================\n\n");
|
||||
|
||||
response.append("Rule ID: ").append(alertDefine.getId()).append("\n");
|
||||
response.append("Name: ").append(alertDefine.getName()).append("\n");
|
||||
response.append("Type: ").append(alertDefine.getType()).append("\n");
|
||||
response.append("Expression: ").append(alertDefine.getExpr()).append("\n");
|
||||
response.append("Trigger Times: ").append(alertDefine.getTimes()).append("\n");
|
||||
response.append("Enabled: ").append(alertDefine.isEnable()).append("\n");
|
||||
|
||||
if (alertDefine.getPeriod() != null) {
|
||||
response.append("Period: ").append(alertDefine.getPeriod()).append(" seconds\n");
|
||||
}
|
||||
|
||||
if (alertDefine.getLabels() != null && !alertDefine.getLabels().isEmpty()) {
|
||||
response.append("Labels: ").append(alertDefine.getLabels()).append("\n");
|
||||
}
|
||||
|
||||
if (alertDefine.getAnnotations() != null && !alertDefine.getAnnotations().isEmpty()) {
|
||||
response.append("Annotations: ").append(alertDefine.getAnnotations()).append("\n");
|
||||
}
|
||||
|
||||
if (alertDefine.getTemplate() != null) {
|
||||
response.append("Template: ").append(alertDefine.getTemplate()).append("\n");
|
||||
}
|
||||
|
||||
response.append("Created: ").append(alertDefine.getGmtCreate()).append("\n");
|
||||
response.append("Modified: ").append(alertDefine.getGmtUpdate()).append("\n");
|
||||
response.append("Creator: ").append(alertDefine.getCreator()).append("\n");
|
||||
response.append("Modifier: ").append(alertDefine.getModifier()).append("\n");
|
||||
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get alert rule details for ID {}: {}", ruleId, e.getMessage(), e);
|
||||
return "Error retrieving alert rule details: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "get_apps_metrics_hierarchy", description = """
|
||||
Get the hierarchical structure of all available apps and their metrics for alert rule creation.
|
||||
This tool provides the exact app name, metric name and corresponding param names according to each metric.
|
||||
Returns structured JSON data showing the complete hierarchy with field parameters for alert expressions.
|
||||
|
||||
JSON Structure:
|
||||
- app: The application name
|
||||
- description: Tool description
|
||||
- hierarchy: Array of hierarchical data
|
||||
- Each node has: value, label, type, description
|
||||
- Leaf nodes have: dataType (numeric/string), unit (if applicable)
|
||||
- Non-leaf nodes have: children array
|
||||
VERY IMPORTANT:
|
||||
- ALWAYS USE the value field from the field parameters when creating alert expressions.
|
||||
|
||||
This structured data is needed to create proper alert expressions.
|
||||
""")
|
||||
public String getAppsMetricsHierarchy(
|
||||
@ToolParam(description = "App/Monitor type to get hierarchy for (e.g., 'linux', 'mysql', 'website')", required = true) String app) {
|
||||
|
||||
try {
|
||||
log.info("Getting apps metrics hierarchy for app: {}", app);
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current subject in get_apps_metrics_hierarchy tool: {}", subjectSum);
|
||||
|
||||
List<Hierarchy> hierarchies;
|
||||
hierarchies = alertDefineServiceAdapter.getAppHierarchy(app.trim().toLowerCase(), "en-US");
|
||||
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
ObjectNode result = mapper.createObjectNode();
|
||||
|
||||
result.put("app", app.toUpperCase());
|
||||
|
||||
if (hierarchies != null && !hierarchies.isEmpty()) {
|
||||
ArrayNode hierarchyArray = mapper.createArrayNode();
|
||||
for (Hierarchy hierarchy : hierarchies) {
|
||||
hierarchyArray.add(UtilityClass.formatHierarchyAsJson(mapper, hierarchy));
|
||||
}
|
||||
result.set("hierarchy", hierarchyArray);
|
||||
} else {
|
||||
result.put("message", "No hierarchy data available");
|
||||
}
|
||||
|
||||
String jsonResult = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result);
|
||||
log.info("Hierarchy JSON: {}", jsonResult);
|
||||
|
||||
return jsonResult;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get apps metrics hierarchy: {}", e.getMessage(), e);
|
||||
return "Error retrieving apps metrics hierarchy: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "bind_monitors_to_alert_rule", description = """
|
||||
Bind monitors to an alert rule.
|
||||
Call this tool if users want to bind specific monitors to their alert rule.
|
||||
Get the right monitor ids for a particular app using the query_monitors tool.
|
||||
Get the alert rule ID from the create_alert_rule tool output OR use the list_alert_rules tool with app_name search filter, if the output of create_alert_rule is not applicable.
|
||||
If monitors are already bound, this will add the new ones to the existing bindings.
|
||||
""")
|
||||
public String bindMonitorsToAlertRule(
|
||||
@ToolParam(description = "Alert rule ID to bind monitors to", required = true) Long ruleId,
|
||||
@ToolParam(description = "Comma-separated list of monitor IDs to bind", required = true) String monitorIds) {
|
||||
try {
|
||||
log.info("Binding monitors to alert rule ID: {}, monitors: {}", ruleId, monitorIds);
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current subject in bind_monitors_to_alert_rule tool: {}", subjectSum);
|
||||
|
||||
if (ruleId == null || ruleId <= 0) {
|
||||
return "Error: Valid alert rule ID is required";
|
||||
}
|
||||
if (monitorIds == null) {
|
||||
return "Error: Monitor IDs are required";
|
||||
}
|
||||
|
||||
// Get the existing alert rule
|
||||
AlertDefine existingRule = alertDefineServiceAdapter.getAlertDefine(ruleId);
|
||||
if (existingRule == null) {
|
||||
return String.format("Error: Alert rule with ID %d not found", ruleId);
|
||||
}
|
||||
|
||||
// Parse monitor IDs from comma-separated string
|
||||
String[] monitorIdArray = monitorIds.split(",");
|
||||
List<String> validMonitorIds = new ArrayList<>();
|
||||
|
||||
for (String monitorId : monitorIdArray) {
|
||||
String trimmedId = monitorId.trim();
|
||||
if (!trimmedId.isEmpty()) {
|
||||
try {
|
||||
Long.parseLong(trimmedId); // Validate it's a number
|
||||
validMonitorIds.add(trimmedId);
|
||||
} catch (NumberFormatException e) {
|
||||
return String.format("Error: Invalid monitor ID '%s'. Monitor IDs must be numeric.", trimmedId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (validMonitorIds.isEmpty()) {
|
||||
return "Error: No valid monitor IDs provided";
|
||||
}
|
||||
|
||||
// Build the monitor instance condition
|
||||
String monitorCondition;
|
||||
if (validMonitorIds.size() == 1) {
|
||||
monitorCondition = String.format("equals(__instance__, \"%s\")", validMonitorIds.get(0));
|
||||
} else {
|
||||
StringBuilder conditionBuilder = new StringBuilder("(");
|
||||
for (int i = 0; i < validMonitorIds.size(); i++) {
|
||||
if (i > 0) {
|
||||
conditionBuilder.append(" or ");
|
||||
}
|
||||
conditionBuilder.append(String.format("equals(__instance__, \"%s\")", validMonitorIds.get(i)));
|
||||
}
|
||||
conditionBuilder.append(")");
|
||||
monitorCondition = conditionBuilder.toString();
|
||||
}
|
||||
|
||||
// Get the current expression and modify it
|
||||
String currentExpr = existingRule.getExpr();
|
||||
String newExpr;
|
||||
|
||||
// Check if the expression already has __instance__ conditions
|
||||
if (currentExpr.contains("__instance__")) {
|
||||
// Extract existing monitor IDs and merge with new ones
|
||||
List<String> existingMonitorIds = UtilityClass.extractExistingMonitorIds(currentExpr);
|
||||
|
||||
// Add new monitor IDs that aren't already present
|
||||
for (String newId : validMonitorIds) {
|
||||
if (!existingMonitorIds.contains(newId)) {
|
||||
existingMonitorIds.add(newId);
|
||||
}
|
||||
}
|
||||
|
||||
String updatedMonitorCondition;
|
||||
if (existingMonitorIds.size() == 1) {
|
||||
updatedMonitorCondition = String.format("equals(__instance__, \"%s\")", existingMonitorIds.get(0));
|
||||
} else {
|
||||
StringBuilder conditionBuilder = new StringBuilder("(");
|
||||
for (int i = 0; i < existingMonitorIds.size(); i++) {
|
||||
if (i > 0) {
|
||||
conditionBuilder.append(" or ");
|
||||
}
|
||||
conditionBuilder.append(String.format("equals(__instance__, \"%s\")", existingMonitorIds.get(i)));
|
||||
}
|
||||
conditionBuilder.append(")");
|
||||
updatedMonitorCondition = conditionBuilder.toString();
|
||||
}
|
||||
|
||||
// Replace existing __instance__ conditions with updated ones
|
||||
newExpr = UtilityClass.replaceInstanceConditions(currentExpr, updatedMonitorCondition);
|
||||
|
||||
// Update the alert rule
|
||||
existingRule.setExpr(newExpr);
|
||||
alertDefineServiceAdapter.modifyAlertDefine(existingRule);
|
||||
|
||||
log.info("Successfully added monitors {} to existing bindings for alert rule ID: {}", validMonitorIds, ruleId);
|
||||
return String.format("Successfully added %d new monitor(s) to alert rule ID %d.\nTotal bound monitors: %s\nUpdated expression: %s",
|
||||
validMonitorIds.size(), ruleId, String.join(", ", existingMonitorIds), newExpr);
|
||||
}
|
||||
|
||||
// Insert the monitor condition after the metrics condition
|
||||
// Pattern: equals(__app__,"app") && equals(__metrics__,"metric") && [existing_conditions]
|
||||
// Result: equals(__app__,"app") && equals(__metrics__,"metric") && [monitor_condition] && [existing_conditions]
|
||||
|
||||
if (currentExpr.matches(".*equals\\(__app__,\"[^\"]+\"\\)\\s*&&\\s*equals\\(__metrics__,\"[^\"]+\"\\)\\s*&&\\s*.*")) {
|
||||
// Find the position after the metrics condition
|
||||
String metricsPattern = "equals\\(__metrics__,\"[^\"]+\"\\)";
|
||||
java.util.regex.Pattern regex = java.util.regex.Pattern.compile(metricsPattern);
|
||||
java.util.regex.Matcher matcher = regex.matcher(currentExpr);
|
||||
|
||||
if (matcher.find()) {
|
||||
int metricsEnd = matcher.end();
|
||||
// Find the " && " after the metrics condition
|
||||
int andPosition = currentExpr.indexOf(" && ", metricsEnd);
|
||||
if (andPosition != -1) {
|
||||
String beforeAndPosition = currentExpr.substring(0, andPosition + 4); // Include " && "
|
||||
String afterAndPosition = currentExpr.substring(andPosition + 4); // Everything after " && "
|
||||
newExpr = beforeAndPosition + monitorCondition + " && " + afterAndPosition;
|
||||
} else {
|
||||
return String.format("Error: Unable to find field conditions after metrics in expression: %s", currentExpr);
|
||||
}
|
||||
} else {
|
||||
return String.format("Error: Unable to parse metrics condition in expression: %s", currentExpr);
|
||||
}
|
||||
} else {
|
||||
return String.format("Error: Expression format not supported for monitor binding: %s", currentExpr);
|
||||
}
|
||||
|
||||
// Update the alert rule
|
||||
existingRule.setExpr(newExpr);
|
||||
alertDefineServiceAdapter.modifyAlertDefine(existingRule);
|
||||
|
||||
log.info("Successfully bound monitors {} to alert rule ID: {}", validMonitorIds, ruleId);
|
||||
return String.format("Successfully bound %d monitor(s) to alert rule ID %d.\nMonitor IDs: %s\nUpdated expression: %s",
|
||||
validMonitorIds.size(), ruleId, String.join(", ", validMonitorIds), newExpr);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to bind monitors to alert rule ID {}: {}", ruleId, e.getMessage(), e);
|
||||
return String.format("Error binding monitors to alert rule: %s", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validates that the app, metric, and field conditions are valid according to hierarchy
|
||||
* @param app App name to validate
|
||||
* @param metrics Metric name to validate for the app
|
||||
* @param fieldConditions Field conditions to validate for the metric
|
||||
* @return "VALID" if all relationships are correct, error message otherwise
|
||||
*/
|
||||
private String validateHierarchyRelationships(String app, String metrics, String fieldConditions) {
|
||||
try {
|
||||
log.debug("Validating hierarchy relationships: app={}, metrics={}, fieldConditions={}", app, metrics, fieldConditions);
|
||||
|
||||
// Get hierarchy for the specified app
|
||||
List<Hierarchy> hierarchies = alertDefineServiceAdapter.getAppHierarchy(app.toLowerCase(), "en-US");
|
||||
|
||||
if (hierarchies == null || hierarchies.isEmpty()) {
|
||||
return String.format("Error: App '%s' not found in hierarchy. Please use list_monitor_types to get valid app names.", app);
|
||||
}
|
||||
|
||||
// Find the metric in the app's hierarchy
|
||||
Hierarchy metricHierarchy = UtilityClass.findMetricInHierarchy(hierarchies, metrics);
|
||||
if (metricHierarchy == null) {
|
||||
return String.format("Error: Metric '%s' not found for app '%s'. Please use get_apps_metrics_hierarchy to get valid metrics for this app.", metrics, app);
|
||||
}
|
||||
|
||||
// Extract field names from field conditions and validate them
|
||||
List<String> fieldNames = UtilityClass.extractFieldNamesFromConditions(fieldConditions);
|
||||
for (String fieldName : fieldNames) {
|
||||
if (!UtilityClass.isFieldValidForMetric(metricHierarchy, fieldName)) {
|
||||
return String.format("Error: Field '%s' not found for metric '%s' in app '%s'. Please use get_apps_metrics_hierarchy to get valid field parameters.", fieldName, metrics, app);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("Hierarchy validation passed for app={}, metrics={}", app, metrics);
|
||||
return "VALID";
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error during hierarchy validation: {}", e.getMessage(), e);
|
||||
return String.format("Error: Unable to validate hierarchy relationships: %s", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+222
-2
@@ -18,8 +18,228 @@
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.tools.impl;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.adapters.AlertServiceAdapter;
|
||||
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
|
||||
import org.apache.hertzbeat.ai.agent.tools.AlertTools;
|
||||
import org.apache.hertzbeat.ai.agent.utils.UtilityClass;
|
||||
import org.apache.hertzbeat.alert.dto.AlertSummary;
|
||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
|
||||
/**
|
||||
* Implementation of Alert Tools functionality
|
||||
* Implementation of Alert Tools functionality for alarm data queries and management
|
||||
*/
|
||||
public class AlertToolsImpl {
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AlertToolsImpl implements AlertTools {
|
||||
@Autowired
|
||||
private AlertServiceAdapter alertServiceAdapter;
|
||||
|
||||
@Override
|
||||
@Tool(name = "query_alerts", description = """
|
||||
Query alerts with comprehensive filtering and pagination options.
|
||||
|
||||
ALERT TYPES:
|
||||
- Pass alertType='single' for individual alert instances
|
||||
- Pass alertType='group' for grouped/aggregated alerts
|
||||
- Pass alertType='both' to get both types (separate sections)
|
||||
|
||||
STATUS FILTERING:
|
||||
- 'firing': Currently active alerts requiring attention
|
||||
- 'resolved': Previously active alerts that have been cleared
|
||||
- 'all': Both firing and resolved alerts (default)
|
||||
|
||||
SEARCH & FILTERING:
|
||||
- search: Search in alert content, labels, or descriptions
|
||||
- sort: Order by 'startAt' (trigger time), 'triggerTimes' (frequency), 'status'
|
||||
- order: 'asc' (oldest first) or 'desc' (newest first, default)
|
||||
|
||||
PAGINATION:
|
||||
- pageIndex: Page number starting from 0
|
||||
- pageSize: Number of alerts per page (default: 10, max recommended: 50)
|
||||
|
||||
EXAMPLE AND COMMON USE CASES:
|
||||
- Recent active alerts: alertType='single', status='firing', sort='startAt', order='desc'
|
||||
- Historical analysis: alertType='single', status='resolved', pageSize=50
|
||||
- Alert grouping overview: alertType='group', status='all'
|
||||
- Search specific issues: search='cpu', alertType='single', status='firing'
|
||||
- Find abnormal monitors: status='firing' to get active alerts indicating monitor issues
|
||||
- Monitor-specific alerts: use search parameter with monitor ID or name to find related alerts
|
||||
- Frequent alerts analysis: sort='triggerTimes', order='desc' to find most frequently triggered alerts
|
||||
- Recent recurring issues: status='all', sort='triggerTimes', order='desc', pageSize=20
|
||||
""")
|
||||
public String queryAlerts(
|
||||
@ToolParam(description = "Alert type: 'single' (individual alerts), 'group' (grouped alerts), 'both' (default: single)", required = false) String alertType,
|
||||
@ToolParam(description = "Alert status: 'firing' (active), 'resolved' (cleared), 'all' (default: all)", required = false) String status,
|
||||
@ToolParam(description = "Search term for alert content or labels", required = false) String search,
|
||||
@ToolParam(description = "Sort field: 'startAt', 'triggerTimes', 'status' (default: startAt)", required = false) String sort,
|
||||
@ToolParam(description = "Sort order: 'asc' or 'desc' (default: desc)", required = false) String order,
|
||||
@ToolParam(description = "Page index starting from 0 (default: 0)", required = false) Integer pageIndex,
|
||||
@ToolParam(description = "Page size, 1-50 recommended (default: 10)", required = false) Integer pageSize) {
|
||||
|
||||
try {
|
||||
log.info("Querying alerts: alertType={}, status={}, search={}, sort={}, order={}", alertType, status, search, sort, order);
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current subject in query_alerts tool: {}", subjectSum);
|
||||
|
||||
// Set defaults
|
||||
if (alertType == null || alertType.trim().isEmpty()) {
|
||||
alertType = "single";
|
||||
}
|
||||
if (status == null || status.trim().isEmpty()) {
|
||||
status = "all";
|
||||
}
|
||||
if (sort == null || sort.trim().isEmpty()) {
|
||||
sort = "startAt";
|
||||
}
|
||||
if (order == null || order.trim().isEmpty()) {
|
||||
order = "desc";
|
||||
}
|
||||
if (pageIndex == null) {
|
||||
pageIndex = 0;
|
||||
}
|
||||
if (pageSize == null) {
|
||||
pageSize = 10;
|
||||
}
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("ALERT QUERY RESULTS\n");
|
||||
response.append("===================\n\n");
|
||||
|
||||
// Handle different alert types
|
||||
if ("single".equalsIgnoreCase(alertType) || "both".equalsIgnoreCase(alertType)) {
|
||||
Page<SingleAlert> singleResult = alertServiceAdapter.getSingleAlerts(status, search, sort, order, pageIndex, pageSize);
|
||||
|
||||
response.append("SINGLE ALERTS:\n");
|
||||
response.append("Found ").append(singleResult.getContent().size()).append(" single alerts (Total: ").append(singleResult.getTotalElements()).append("):\n\n");
|
||||
|
||||
for (SingleAlert alert : singleResult.getContent()) {
|
||||
response.append("Alert ID: ").append(alert.getId()).append("\n");
|
||||
response.append("Status: ").append(alert.getStatus()).append("\n");
|
||||
response.append("Content: ").append(alert.getContent() != null ? alert.getContent() : "No content").append("\n");
|
||||
response.append("Trigger Times: ").append(alert.getTriggerTimes()).append("\n");
|
||||
|
||||
if (alert.getStartAt() != null) {
|
||||
response.append("Started At: ").append(UtilityClass.formatTimestamp(alert.getStartAt())).append("\n");
|
||||
}
|
||||
if (alert.getActiveAt() != null) {
|
||||
response.append("Active At: ").append(UtilityClass.formatTimestamp(alert.getActiveAt())).append("\n");
|
||||
}
|
||||
if (alert.getEndAt() != null) {
|
||||
response.append("Ended At: ").append(UtilityClass.formatTimestamp(alert.getEndAt())).append("\n");
|
||||
}
|
||||
|
||||
if (alert.getLabels() != null && !alert.getLabels().isEmpty()) {
|
||||
response.append("Labels: ").append(alert.getLabels()).append("\n");
|
||||
}
|
||||
response.append("\n");
|
||||
}
|
||||
|
||||
if (singleResult.getContent().isEmpty()) {
|
||||
response.append("No single alerts found matching the specified criteria.\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle group alerts
|
||||
if ("group".equalsIgnoreCase(alertType) || "both".equalsIgnoreCase(alertType)) {
|
||||
if ("both".equalsIgnoreCase(alertType)) {
|
||||
response.append("\n");
|
||||
}
|
||||
|
||||
Page<GroupAlert> groupResult = alertServiceAdapter.getGroupAlerts(status, search, sort, order, pageIndex, pageSize);
|
||||
|
||||
response.append("GROUP ALERTS:\n");
|
||||
response.append("Found ").append(groupResult.getContent().size()).append(" group alerts (Total: ").append(groupResult.getTotalElements()).append("):\n\n");
|
||||
|
||||
for (GroupAlert alert : groupResult.getContent()) {
|
||||
response.append("Group Alert ID: ").append(alert.getId()).append("\n");
|
||||
response.append("Status: ").append(alert.getStatus()).append("\n");
|
||||
response.append("Group Key: ").append(alert.getGroupKey() != null ? alert.getGroupKey() : "No group key").append("\n");
|
||||
|
||||
if (alert.getGmtCreate() != null) {
|
||||
response.append("Created At: ").append(alert.getGmtCreate()).append("\n");
|
||||
}
|
||||
if (alert.getGmtUpdate() != null) {
|
||||
response.append("Updated At: ").append(alert.getGmtUpdate()).append("\n");
|
||||
}
|
||||
|
||||
if (alert.getCommonLabels() != null && !alert.getCommonLabels().isEmpty()) {
|
||||
response.append("Common Labels: ").append(alert.getCommonLabels()).append("\n");
|
||||
}
|
||||
if (alert.getCommonAnnotations() != null && !alert.getCommonAnnotations().isEmpty()) {
|
||||
response.append("Annotations: ").append(alert.getCommonAnnotations()).append("\n");
|
||||
}
|
||||
response.append("\n");
|
||||
}
|
||||
|
||||
if (groupResult.getContent().isEmpty()) {
|
||||
response.append("No group alerts found matching the specified criteria.\n");
|
||||
}
|
||||
}
|
||||
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to query alerts: {}", e.getMessage(), e);
|
||||
return "Error retrieving alerts: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Tool(name = "get_alerts_summary", description = """
|
||||
Get alerts summary statistics including total counts, status distribution, and recent trends.
|
||||
Returns comprehensive overview of the current alerting status across all monitors.
|
||||
""")
|
||||
public String getAlertsSummary() {
|
||||
try {
|
||||
log.info("Getting alerts summary");
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current subject in get_alerts_summary tool: {}", subjectSum);
|
||||
|
||||
AlertSummary summary = alertServiceAdapter.getAlertsSummary();
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("ALERTS SUMMARY\n");
|
||||
response.append("==============\n\n");
|
||||
|
||||
if (summary != null) {
|
||||
response.append("Total Alerts: ").append(summary.getTotal()).append("\n");
|
||||
response.append("Handled Alerts: ").append(summary.getDealNum()).append("\n");
|
||||
response.append("Handling Rate: ").append(String.format("%.1f", summary.getRate())).append("%\n\n");
|
||||
|
||||
response.append("Priority Breakdown (Unhandled):\n");
|
||||
response.append("- Critical: ").append(summary.getPriorityCriticalNum()).append("\n");
|
||||
response.append("- Emergency: ").append(summary.getPriorityEmergencyNum()).append("\n");
|
||||
response.append("- Warning: ").append(summary.getPriorityWarningNum()).append("\n\n");
|
||||
|
||||
long totalUnhandled = summary.getPriorityCriticalNum() + summary.getPriorityEmergencyNum() + summary.getPriorityWarningNum();
|
||||
response.append("Total Unhandled Alerts: ").append(totalUnhandled).append("\n");
|
||||
|
||||
if (totalUnhandled > 0) {
|
||||
response.append("\nUnhandled Alert Distribution:\n");
|
||||
response.append("- Critical: ").append(String.format("%.1f", (summary.getPriorityCriticalNum() * 100.0 / totalUnhandled))).append("%\n");
|
||||
response.append("- Emergency: ").append(String.format("%.1f", (summary.getPriorityEmergencyNum() * 100.0 / totalUnhandled))).append("%\n");
|
||||
response.append("- Warning: ").append(String.format("%.1f", (summary.getPriorityWarningNum() * 100.0 / totalUnhandled))).append("%\n");
|
||||
}
|
||||
} else {
|
||||
response.append("No alert summary data available.");
|
||||
}
|
||||
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get alerts summary: {}", e.getMessage(), e);
|
||||
return "Error retrieving alerts summary: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+241
-3
@@ -18,8 +18,246 @@
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.tools.impl;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.adapters.MetricsServiceAdapter;
|
||||
import org.apache.hertzbeat.ai.agent.adapters.MonitorServiceAdapter;
|
||||
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
|
||||
import org.apache.hertzbeat.ai.agent.tools.MetricsTools;
|
||||
import org.apache.hertzbeat.common.entity.dto.Field;
|
||||
import org.apache.hertzbeat.common.entity.dto.MetricsData;
|
||||
import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData;
|
||||
import org.apache.hertzbeat.common.entity.dto.Value;
|
||||
import org.apache.hertzbeat.common.entity.dto.ValueRow;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Implementation of Metrics Tools functionality
|
||||
* Implementation of Metrics Tools functionality for metrics data queries and analysis
|
||||
*/
|
||||
public class MetricsToolsImpl {
|
||||
}
|
||||
@Slf4j
|
||||
@Service
|
||||
public class MetricsToolsImpl implements MetricsTools {
|
||||
@Autowired
|
||||
private MetricsServiceAdapter metricsServiceAdapter;
|
||||
@Autowired
|
||||
private MonitorServiceAdapter monitorServiceAdapter;
|
||||
|
||||
@Override
|
||||
@Tool(name = "query_realtime_metrics", description = """
|
||||
Get the supported monitor types/names from the list_monitor_types tool, make sure to use right name in the next call
|
||||
Use the query_monitors tool to find monitor IDs in case the user does not tell the id explicitly. You might have to use this multiple times based on the user's query
|
||||
Get real-time metrics data for a specific monitor.
|
||||
Returns current metrics values including CPU, memory, disk usage, etc.
|
||||
Based on the monitor type/name, use the get_apps_metrics_hierarchy tool to get the metrics hierarchy. i.e., metrics and the field parameter (sub-metric).
|
||||
Each metric has its submetrics as well for example: cpu has field parameters or sub-metrics like 'usage', 'load', 'core'. These value might be numeric or string
|
||||
User might ask about specific metrics like 'cpu usage', 'memory used', 'disk available', etc.
|
||||
So use this tool to get the real-time metrics data for the monitor- user asked the specific metrics for, along with any logical or numeric conditions if the user mentions
|
||||
In case of multiple monitors matching the user's description, you might have to call query_realtime_metrics tool mutliple times with different parameters.
|
||||
|
||||
|
||||
EXAMPLE WORKFLOW
|
||||
|
||||
If the user asks for 'cpu usage' for a particular monitor or multiple matching monitors like 'web server', 'database server', etc.
|
||||
Get the closest matching monitor name from list_monitor_types tool.
|
||||
Call query_monitors tool to get the monitor ID/IDs with the obtained monitor type
|
||||
Call get_apps_metrics_hierarchy tool to get all the metrics (type=metric) for the obtained monitor type
|
||||
Call query_realtime_metrics tool with each of the IDs and the closest matching metrics name (e.g., 'cpu', 'memory', etc.)
|
||||
From the result, do whatever operation user wants you to do with the data. In this example case display the cpu usage for each matching monitor.
|
||||
""")
|
||||
public String getRealtimeMetrics(
|
||||
@ToolParam(description = "Monitor ID", required = true) Long monitorId,
|
||||
@ToolParam(description = "Metrics name (e.g., 'system', 'cpu', 'memory') obtained from get_apps_metrics_hierarchy result", required = true) String metrics) {
|
||||
try {
|
||||
log.info("Getting real-time metrics for monitor {} and metrics {}", monitorId, metrics);
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current subject in get_realtime_metrics tool: {}", subjectSum);
|
||||
|
||||
MetricsData metricsData = metricsServiceAdapter.getMetricsData(monitorId, metrics);
|
||||
|
||||
if (metricsData == null) {
|
||||
return String.format("No real-time metrics data found for monitor ID %d and metrics '%s'", monitorId, metrics);
|
||||
}
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("REAL-TIME METRICS DATA\n");
|
||||
response.append("=".repeat(50)).append("\n");
|
||||
response.append("Monitor ID: ").append(monitorId).append("\n");
|
||||
response.append("Metrics: ").append(metrics).append("\n");
|
||||
response.append("=".repeat(50)).append("\n\n");
|
||||
|
||||
if (metricsData.getValueRows() != null && !metricsData.getValueRows().isEmpty()) {
|
||||
List<Field> fields = metricsData.getFields();
|
||||
|
||||
response.append("Available Field Parameters (Sub-metrics):\n");
|
||||
response.append("-".repeat(40)).append("\n");
|
||||
|
||||
for (ValueRow valueRow : metricsData.getValueRows()) {
|
||||
// Show labels if available
|
||||
if (valueRow.getLabels() != null && !valueRow.getLabels().isEmpty()) {
|
||||
response.append("Instance Labels: ").append(valueRow.getLabels()).append("\n");
|
||||
response.append("-".repeat(20)).append("\n");
|
||||
}
|
||||
|
||||
List<Value> values = valueRow.getValues();
|
||||
for (int i = 0; i < values.size() && i < fields.size(); i++) {
|
||||
Field field = fields.get(i);
|
||||
Value value = values.get(i);
|
||||
|
||||
// Enhanced field information display
|
||||
response.append("• Field Parameter: ").append(field.getName());
|
||||
if (field.getUnit() != null && !field.getUnit().isEmpty()) {
|
||||
response.append(" (").append(field.getUnit()).append(")");
|
||||
}
|
||||
response.append("\n");
|
||||
|
||||
response.append(" Current Value: ").append(value.getOrigin());
|
||||
|
||||
// Add data type indication
|
||||
try {
|
||||
Double.parseDouble(value.getOrigin());
|
||||
response.append(" [Numeric]");
|
||||
} catch (NumberFormatException e) {
|
||||
response.append(" [String]");
|
||||
}
|
||||
response.append("\n\n");
|
||||
}
|
||||
|
||||
if (valueRow.getLabels() != null && !valueRow.getLabels().isEmpty()) {
|
||||
response.append("-".repeat(20)).append("\n");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response.append("No field parameter data available for metrics '").append(metrics).append("'.\n");
|
||||
response.append("Use get_apps_metrics_hierarchy tool to check available metrics for this monitor type.");
|
||||
}
|
||||
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get real-time metrics: {}", e.getMessage(), e);
|
||||
return "Error retrieving real-time metrics: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "get_historical_metrics", description = """
|
||||
Get historical metrics data for analysis and trending.
|
||||
Returns time-series data for specified metrics over a time range.
|
||||
Use the query_monitors tool to find the correct monitor IDs/ name or type for the monitor(s) user asked the metrics for
|
||||
Pass that name into the get_apps_metrics_hierarchy tool to get the metrics hierarchy i.e metrics and the field paramater
|
||||
DO NOT USE THE LABEL FIELD ALWAYS USE THE VALUE FIELD FROM THE HIERARCHY JSON
|
||||
Ask user to provide the filters for labels, history and interval aggregation
|
||||
""")
|
||||
public String getHistoricalMetrics(
|
||||
@ToolParam(description = "Monitor ID", required = true) Long monitorId,
|
||||
@ToolParam(description = "Monitor type (e.g., 'linux', 'mysql', 'http')", required = true) String app,
|
||||
@ToolParam(description = "Metrics name (e.g., 'target', 'cpu', 'memory')", required = true) String metrics,
|
||||
@ToolParam(description = "Field Parameter (e.g., 'usage', 'used', 'available')", required = false) String fieldParameter,
|
||||
@ToolParam(description = "Label filter for specific instances", required = false) String label,
|
||||
@ToolParam(description = "Time range (e.g., '1h', '6h', '24h', '7d')", required = false) String history,
|
||||
@ToolParam(description = "Whether to aggregate data with intervals", required = false) Boolean interval) {
|
||||
|
||||
try {
|
||||
log.info("Getting historical metrics for monitor {} and metrics {}", monitorId, metrics);
|
||||
|
||||
if (history == null || history.trim().isEmpty()) {
|
||||
history = "24h";
|
||||
}
|
||||
if (interval == null) {
|
||||
interval = true;
|
||||
}
|
||||
|
||||
MetricsHistoryData historyData = metricsServiceAdapter.getMetricHistoryData(
|
||||
monitorId, app, metrics, fieldParameter, label, history, interval);
|
||||
|
||||
if (historyData == null) {
|
||||
return String.format("No historical metrics data found for monitor ID %d and metrics '%s'", monitorId, metrics);
|
||||
}
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("HISTORICAL METRICS: ").append(metrics).append(" (Monitor ID: ").append(monitorId).append(")\n");
|
||||
response.append("Time Range: ").append(history).append(" | Interval Aggregation: ").append(interval).append("\n");
|
||||
response.append("=".repeat(60)).append("\n\n");
|
||||
|
||||
if (historyData.getValues() != null && !historyData.getValues().isEmpty()) {
|
||||
response.append("Field: ").append(historyData.getField() != null ? historyData.getField().getName() : "Unknown").append("\n");
|
||||
|
||||
// Calculate total data points
|
||||
int totalPoints = historyData.getValues().values().stream()
|
||||
.mapToInt(List::size)
|
||||
.sum();
|
||||
response.append("Data Points: ").append(totalPoints).append("\n");
|
||||
|
||||
// Show sample data points (first 10) from all value lists
|
||||
int count = 0;
|
||||
response.append("\nSample Data Points:\n");
|
||||
for (Map.Entry<String, List<Value>> entry : historyData.getValues().entrySet()) {
|
||||
String labelKey = entry.getKey();
|
||||
List<Value> values = entry.getValue();
|
||||
|
||||
for (Value value : values) {
|
||||
if (count >= 10) break;
|
||||
response.append("Label: ").append(labelKey)
|
||||
.append(" | Time: ").append(value.getTime())
|
||||
.append(" | Value: ").append(value.getOrigin()).append("\n");
|
||||
count++;
|
||||
}
|
||||
if (count >= 10) break;
|
||||
}
|
||||
|
||||
if (totalPoints > 10) {
|
||||
response.append("... and ").append(totalPoints - 10).append(" more data points\n");
|
||||
}
|
||||
} else {
|
||||
response.append("No historical data points available.");
|
||||
}
|
||||
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get historical metrics: {}", e.getMessage(), e);
|
||||
return "Error retrieving historical metrics: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Tool(name = "get_warehouse_status", description = """
|
||||
Check the status of the metrics storage warehouse system.
|
||||
Returns whether the metrics storage is operational and accessible.
|
||||
""")
|
||||
public String getWarehouseStatus() {
|
||||
try {
|
||||
log.info("Checking warehouse storage status");
|
||||
|
||||
Boolean status = metricsServiceAdapter.getWarehouseStorageServerStatus();
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("METRICS WAREHOUSE STATUS\n");
|
||||
response.append("========================\n\n");
|
||||
|
||||
if (status != null && status) {
|
||||
response.append("Status: ONLINE ✓\n");
|
||||
response.append("The metrics storage warehouse is operational and accessible.\n");
|
||||
response.append("Historical metrics data queries are available.");
|
||||
} else {
|
||||
response.append("Status: OFFLINE ✗\n");
|
||||
response.append("The metrics storage warehouse is not accessible.\n");
|
||||
response.append("Only real-time metrics may be available.");
|
||||
}
|
||||
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get warehouse status: {}", e.getMessage(), e);
|
||||
return "Error checking warehouse status: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+430
-26
@@ -21,7 +21,7 @@ import com.usthe.sureness.subject.SubjectSum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.adapters.MonitorServiceAdapter;
|
||||
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.apache.hertzbeat.ai.agent.utils.UtilityClass;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -29,8 +29,12 @@ import org.apache.hertzbeat.ai.agent.tools.MonitorTools;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.apache.hertzbeat.common.entity.manager.Monitor;
|
||||
import org.apache.hertzbeat.common.entity.manager.Param;
|
||||
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Implementation of Monitoring Tools functionality
|
||||
@@ -38,7 +42,6 @@ import java.util.List;
|
||||
@Slf4j
|
||||
@Service
|
||||
public class MonitorToolsImpl implements MonitorTools {
|
||||
|
||||
@Autowired
|
||||
private MonitorServiceAdapter monitorServiceAdapter;
|
||||
|
||||
@@ -47,41 +50,442 @@ public class MonitorToolsImpl implements MonitorTools {
|
||||
* Tool to query monitor information with flexible filtering and pagination.
|
||||
* Supports filtering by monitor IDs, type, status, host, labels, sorting, and
|
||||
* pagination.
|
||||
* Returns monitor names as string.
|
||||
* Returns detailed monitor information including ID, name, type, host, and status.
|
||||
*/
|
||||
@Override
|
||||
@Tool(name = "list_monitors", returnDirect = true, description = """
|
||||
Query monitor information with flexible filtering and pagination.
|
||||
Supports filtering by monitor IDs, type, status, host, labels, sorting, and pagination.
|
||||
Returns results as String. When no parameters are available, pass the default value as mentioned below. If the user doesn't provide any specific parameter, the default value will be used.
|
||||
@Tool(name = "query_monitors", description = """
|
||||
Query Existing/configured monitors in HertzBeat.
|
||||
This tool retrieves monitors based on various filters and parameters.
|
||||
Comprehensive monitor querying with flexible filtering, pagination, and specialized views.
|
||||
|
||||
MONITOR STATUSES:
|
||||
- status=1: Online/Active monitors (healthy, responding normally)
|
||||
- status=2: Offline monitors (not responding, connection failed)
|
||||
- status=3: Unreachable monitors (network/connectivity issues)
|
||||
- status=0: Paused monitors (manually disabled/suspended)
|
||||
- status=9 or null: All monitors regardless of status (default)
|
||||
|
||||
COMMON USE CASES & PARAMETER COMBINATIONS:
|
||||
|
||||
1. BASIC MONITOR LISTING:
|
||||
- Default: No parameters (shows all monitors, 8 per page)
|
||||
- By type: app='linux' (show only Linux monitors)
|
||||
- Search: search='web' (find monitors with 'web' in name/host)
|
||||
|
||||
2. STATUS-BASED QUERIES:
|
||||
- Healthy monitors: status=1, pageSize=50
|
||||
- Problem monitors: status=2 or status=3, pageSize=50
|
||||
- Offline monitors only: status=2
|
||||
- Unreachable monitors only: status=3
|
||||
- Paused monitors: status=0
|
||||
|
||||
3. MONITORING HEALTH OVERVIEW:
|
||||
- All statuses with statistics: status=9, includeStats=true, pageSize=100
|
||||
- Unhealthy monitors: Pass both status=2 AND status=3 (make 2 separate calls)
|
||||
|
||||
4. ADVANCED FILTERING:
|
||||
- Specific monitor types: app='mysql', status=1 (healthy MySQL monitors)
|
||||
- Label-based: labels='env:prod,critical:true'
|
||||
- Host search: search='192.168' (find by IP pattern)
|
||||
- Monitor IDs: ids=[1,2,3] (specific monitors by ID)
|
||||
|
||||
5. SORTING & PAGINATION:
|
||||
- Recently updated: sort='gmtUpdate', order='desc'
|
||||
- Alphabetical: sort='name', order='asc'
|
||||
- By creation: sort='gmtCreate', order='desc' (newest first)
|
||||
- Large datasets: pageSize=50-100 for bulk operations
|
||||
|
||||
RESPONSE FORMAT:
|
||||
- includeStats=true: Adds status distribution summary at top
|
||||
- Default: Simple list with ID, name, type, host, status
|
||||
- Shows total count and pagination info
|
||||
""")
|
||||
public String listMonitors(
|
||||
@ToolParam(description = "List of monitor IDs to filter (default: empty list)", required = false) List<Long> ids,
|
||||
@ToolParam(description = "Monitor type, e.g., 'linux' (default: null)", required = false) String app,
|
||||
@ToolParam(description = "Monitor status (0: no monitor, 1: usable, 2: disabled, 9: all) (default: null)", required = false) Byte status,
|
||||
@ToolParam(description = "Fuzzy search for host or name (default: null)", required = false) String search,
|
||||
@ToolParam(description = "Monitor labels, e.g., 'env:prod,instance:22' (default: null)", required = false) String labels,
|
||||
@ToolParam(description = "Sort field, e.g., 'name' (default: gmtCreate)", required = false) String sort,
|
||||
@ToolParam(description = "Sort order, 'asc' or 'desc' (default: desc)", required = false) String order,
|
||||
@ToolParam(description = "Page index (default: 0)", required = false) Integer pageIndex,
|
||||
@ToolParam(description = "Page size (default: 8)", required = false) Integer pageSize,
|
||||
ToolContext context) {
|
||||
public String queryMonitors(
|
||||
@ToolParam(description = "Specific monitor IDs to retrieve (optional)", required = false) List<Long> ids,
|
||||
@ToolParam(description = "Monitor type filter: 'linux', 'mysql', 'http', 'redis', etc. (optional)", required = false) String app,
|
||||
@ToolParam(description = "Monitor status: 1=online, 2=offline, 3=unreachable, 0=paused, 9=all (default: 9)", required = false) Byte status,
|
||||
@ToolParam(description = "Search in monitor names or hosts (partial matching)", required = false) String search,
|
||||
@ToolParam(description = "Label filters, format: 'key1:value1,key2:value2'", required = false) String labels,
|
||||
@ToolParam(description = "Sort field: 'name', 'gmtCreate', 'gmtUpdate', 'status', 'app' (default: gmtCreate)", required = false) String sort,
|
||||
@ToolParam(description = "Sort order: 'asc' (ascending) or 'desc' (descending, default)", required = false) String order,
|
||||
@ToolParam(description = "Page number starting from 0 (default: 0)", required = false) Integer pageIndex,
|
||||
@ToolParam(description = "Items per page: 1-100 recommended (default: 20)", required = false) Integer pageSize,
|
||||
@ToolParam(description = "Include status statistics summary (default: false)", required = false) Boolean includeStats) {
|
||||
try {
|
||||
// Set defaults
|
||||
if (pageSize == null || pageSize <= 0) {
|
||||
pageSize = 20;
|
||||
}
|
||||
if (pageIndex == null) {
|
||||
pageIndex = 0;
|
||||
}
|
||||
if (includeStats == null) {
|
||||
includeStats = false;
|
||||
}
|
||||
|
||||
Page<Monitor> result = monitorServiceAdapter.getMonitors(ids, app, search, status, sort, order, pageIndex, pageSize, labels);
|
||||
log.debug("MonitorServiceAdapter.getMonitors result: {}", result);
|
||||
return result.getContent().stream().map(Monitor::getName).toList().toString();
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("MONITOR QUERY RESULTS\n");
|
||||
response.append("====================\n\n");
|
||||
|
||||
// Include statistics if requested
|
||||
if (includeStats) {
|
||||
// Get status distribution by calling with different status values
|
||||
long onlineCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 1, null, null, 0, 1000, labels).getTotalElements();
|
||||
long offlineCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 2, null, null, 0, 1000, labels).getTotalElements();
|
||||
long unreachableCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 3, null, null, 0, 1000, labels).getTotalElements();
|
||||
long pausedCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 0, null, null, 0, 1000, labels).getTotalElements();
|
||||
|
||||
response.append("STATUS OVERVIEW:\n");
|
||||
response.append("- Online: ").append(onlineCount).append("\n");
|
||||
response.append("- Offline: ").append(offlineCount).append("\n");
|
||||
response.append("- Unreachable: ").append(unreachableCount).append("\n");
|
||||
response.append("- Paused: ").append(pausedCount).append("\n");
|
||||
|
||||
long total = onlineCount + offlineCount + unreachableCount + pausedCount;
|
||||
if (total > 0) {
|
||||
double healthPercentage = (onlineCount * 100.0) / total;
|
||||
response.append("- Health Rate: ").append(String.format("%.1f", healthPercentage)).append("%\n");
|
||||
}
|
||||
response.append("\n");
|
||||
}
|
||||
|
||||
response.append("Query Results: ").append(result.getContent().size())
|
||||
.append(" monitors (Total: ").append(result.getTotalElements()).append(")\n");
|
||||
|
||||
if (result.getTotalPages() > 1) {
|
||||
response.append("Page ").append(pageIndex + 1).append(" of ").append(result.getTotalPages()).append("\n");
|
||||
}
|
||||
response.append("\n");
|
||||
|
||||
for (Monitor monitor : result.getContent()) {
|
||||
response.append("ID: ").append(monitor.getId())
|
||||
.append(" | Name: ").append(monitor.getName())
|
||||
.append(" | Type: ").append(monitor.getApp())
|
||||
.append(" | Host: ").append(monitor.getHost())
|
||||
.append(" | Status: ").append(UtilityClass.getStatusText(monitor.getStatus()));
|
||||
|
||||
// Add creation date for better context
|
||||
if (monitor.getGmtCreate() != null) {
|
||||
response.append(" | Created: ").append(monitor.getGmtCreate().toString(), 0, 10);
|
||||
}
|
||||
response.append("\n");
|
||||
}
|
||||
|
||||
if (result.getContent().isEmpty()) {
|
||||
response.append("No monitors found matching the specified criteria.\n");
|
||||
response.append("Try adjusting your filters or search terms.");
|
||||
}
|
||||
|
||||
return response.toString();
|
||||
} catch (Exception e) {
|
||||
return "error is" + e.getMessage();
|
||||
return "Error retrieving monitors: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Tool(name = "add_monitor", description = "Add a new monitor")
|
||||
public String addMonitor(@ToolParam(description = "Name of the monitor") String name, ToolContext context) {
|
||||
log.debug("Adding monitor with name: {}", name);
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current subject in tool: {}", subjectSum);
|
||||
return "Monitor added: " + name;
|
||||
@Tool(name = "add_monitor", description = """
|
||||
Add a new monitoring target to HertzBeat with comprehensive configuration.
|
||||
This tool dynamically handles different parameter requirements for each monitor type.
|
||||
|
||||
This tool creates monitors with proper app-specific parameters.
|
||||
|
||||
*********
|
||||
VERY IMPORTANT:
|
||||
ALWAYS use get_monitor_additional_params to check the additional required parameters for the chosen type before adding a monitor or even mentioning it.
|
||||
Use list_monitor_types tool to see available monitor type names to use here in the app parameter.
|
||||
Use the information obtained from this to query user for parameters.
|
||||
If the User has not given any parameters, ask them to provide the necessary parameters, until all the necessary parameters are provided.
|
||||
**********
|
||||
|
||||
Examples of natural language requests this tool handles:
|
||||
- "Monitor website example.com with HTTPS on port 443"
|
||||
- "Add MySQL monitoring for database server at 192.168.1.10 with user admin"
|
||||
- "Monitor Linux server health on host server.company.com via SSH"
|
||||
- "Set up Redis monitoring on localhost port 6379 with password"
|
||||
|
||||
PARAMETER MAPPING: The tool intelligently maps common parameters:
|
||||
- host: Target server/domain
|
||||
- port: Service port (auto-detected if not specified)
|
||||
- username: Authentication username
|
||||
- password: Authentication password
|
||||
- database: Database name (for DB monitors)
|
||||
- additionalParams: JSON string for app-specific parameters (to be obtained from get_monitor_param_defines)
|
||||
|
||||
ADDITIONAL PARAMETERS EXAMPLES:
|
||||
- Website: {"uri":"/api/health", "ssl":"true", "method":"POST"}
|
||||
- Linux: {"privateKey":"ssh-key-content", "script":"custom-script"}
|
||||
- Database: {"url":"jdbc:mysql://custom", "timeout":"10000"}
|
||||
""")
|
||||
public String addMonitor(
|
||||
@ToolParam(description = "Monitor name (required)", required = true) String name,
|
||||
@ToolParam(description = "Monitor type: website, mysql, postgresql, redis, linux, windows, etc.", required = true) String app,
|
||||
@ToolParam(description = "Target host: IP address or domain name", required = true) String host,
|
||||
@ToolParam(description = "Target port (optional, auto-detected if not specified)", required = false) Integer port,
|
||||
@ToolParam(description = "Collection interval in seconds (default: 600)", required = false) Integer intervals,
|
||||
@ToolParam(description = "Username for authentication (optional)", required = false) String username,
|
||||
@ToolParam(description = "Password for authentication (optional)", required = false) String password,
|
||||
@ToolParam(description = "Database name (for database monitors)", required = false) String database,
|
||||
@ToolParam(description = "Additional app-specific parameters as JSON: {\"uri\":\"/api\", \"ssl\":\"true\", \"method\":\"POST\"}", required = false) String additionalParams,
|
||||
@ToolParam(description = "Monitor description (optional)", required = false) String description) {
|
||||
|
||||
try {
|
||||
log.info("Adding monitor: name={}, app={}, host={}", name, app, host);
|
||||
|
||||
// Validate required parameters
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
return "Error: Monitor name is required";
|
||||
}
|
||||
if (app == null || app.trim().isEmpty()) {
|
||||
return "Error: Monitor type/application is required";
|
||||
}
|
||||
if (host == null || host.trim().isEmpty()) {
|
||||
return "Error: Host is required";
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if (intervals == null || intervals < 10) {
|
||||
intervals = 600;
|
||||
}
|
||||
|
||||
// Create Monitor entity
|
||||
Monitor monitor = Monitor.builder()
|
||||
.name(name.trim())
|
||||
.app(app.toLowerCase().trim())
|
||||
.host(host.trim())
|
||||
.intervals(intervals)
|
||||
.status((byte) 1)
|
||||
.type((byte) 0)
|
||||
.description(description != null ? description.trim() : "")
|
||||
.build();
|
||||
|
||||
List<Param> params = createBasicParams(host, port, username, password, database, additionalParams);
|
||||
|
||||
// Validate that all required parameters for this monitor type are provided
|
||||
try {
|
||||
List<ParamDefine> requiredParams = monitorServiceAdapter.getMonitorParamDefines(app);
|
||||
log.info("Checking required parameters for monitor type '{}': {}", app, requiredParams);
|
||||
List<String> missingParams = new ArrayList<>();
|
||||
|
||||
for (ParamDefine paramDefine : requiredParams) {
|
||||
if (paramDefine.isRequired()) {
|
||||
String fieldName = paramDefine.getField();
|
||||
boolean hasParam = params.stream()
|
||||
.anyMatch(param -> fieldName.equals(param.getField()));
|
||||
if (!hasParam) {
|
||||
missingParams.add(fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!missingParams.isEmpty()) {
|
||||
return String.format("Error: Missing required parameters for monitor type '%s': %s. "
|
||||
+ "Use get_monitor_additional_params tool to see all required parameters.",
|
||||
app, String.join(", ", missingParams));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Could not validate required parameters for monitor type '{}': {}", app, e.getMessage());
|
||||
}
|
||||
|
||||
// Call adapter - it handles all the complexity (validation, defaults, app-specific logic)
|
||||
Long monitorId = monitorServiceAdapter.addMonitor(monitor, params, null);
|
||||
|
||||
log.info("Successfully added monitor '{}' with ID: {}", name, monitorId);
|
||||
return String.format("Successfully added %s monitor '%s' with ID: %d (Host: %s, Interval: %d seconds)",
|
||||
app.toUpperCase(), name, monitorId, host, intervals);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to add monitor '{}': {}", name, e.getMessage(), e);
|
||||
return "Error adding monitor '" + name + "': " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create basic parameter list from user inputs
|
||||
*/
|
||||
private List<Param> createBasicParams(String host, Integer port, String username,
|
||||
String password, String database, String additionalParams) {
|
||||
List<Param> params = new ArrayList<>();
|
||||
|
||||
// Add host (always required)
|
||||
params.add(Param.builder().field("host").paramValue(host.trim()).type((byte) 1).build());
|
||||
|
||||
// Add optional common parameters
|
||||
if (port != null) {
|
||||
params.add(Param.builder().field("port").paramValue(port.toString()).type((byte) 0).build());
|
||||
}
|
||||
if (username != null && !username.trim().isEmpty()) {
|
||||
params.add(Param.builder().field("username").paramValue(username.trim()).type((byte) 1).build());
|
||||
}
|
||||
if (password != null && !password.trim().isEmpty()) {
|
||||
params.add(Param.builder().field("password").paramValue(password.trim()).type((byte) 2).build());
|
||||
}
|
||||
if (database != null && !database.trim().isEmpty()) {
|
||||
params.add(Param.builder().field("database").paramValue(database.trim()).type((byte) 1).build());
|
||||
}
|
||||
|
||||
// Parse additional parameters if provided
|
||||
if (additionalParams != null && !additionalParams.trim().isEmpty()) {
|
||||
try {
|
||||
String cleaned = additionalParams.trim().replaceAll("[{}]", "");
|
||||
String[] pairs = cleaned.split(",");
|
||||
for (String pair : pairs) {
|
||||
String[] keyValue = pair.split(":");
|
||||
if (keyValue.length == 2) {
|
||||
String key = keyValue[0].trim().replaceAll("\"", "");
|
||||
String value = keyValue[1].trim().replaceAll("\"", "");
|
||||
params.add(Param.builder().field(key).paramValue(value).type((byte) 1).build());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to parse additionalParams: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "list_monitor_types", description = """
|
||||
List all available monitor types that can be added to HertzBeat.
|
||||
This tool shows all supported monitor types with their display names.
|
||||
Use this to see what types of monitors you can create with the add_monitor tool.
|
||||
""")
|
||||
public String listMonitorTypes(
|
||||
@ToolParam(description = "Language code for localized names (en-US, zh-CN, etc.). Default: en-US", required = false) String language) {
|
||||
|
||||
try {
|
||||
log.info("Listing available monitor types for language: {}", language);
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current subject in list_monitor_types tool: {}", subjectSum);
|
||||
|
||||
// Set default language if not provided
|
||||
if (language == null || language.trim().isEmpty()) {
|
||||
language = "en-US";
|
||||
}
|
||||
|
||||
// Get available monitor types from adapter
|
||||
Map<String, String> monitorTypes = monitorServiceAdapter.getAvailableMonitorTypes(language);
|
||||
|
||||
if (monitorTypes == null || monitorTypes.isEmpty()) {
|
||||
return "No monitor types are currently available.";
|
||||
}
|
||||
|
||||
// Format the response as a nice list
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("Available Monitor Types (Total: ").append(monitorTypes.size()).append("):\n\n");
|
||||
|
||||
// Sort monitor types alphabetically by key
|
||||
List<Map.Entry<String, String>> sortedTypes = monitorTypes.entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.toList();
|
||||
|
||||
for (Map.Entry<String, String> entry : sortedTypes) {
|
||||
String typeKey = entry.getKey();
|
||||
String displayName = entry.getValue();
|
||||
response.append("• ").append(typeKey)
|
||||
.append(" - ").append(displayName)
|
||||
.append("\n");
|
||||
}
|
||||
|
||||
response.append("\nTo add a monitor, use the add_monitor tool with one of these types as the 'app' parameter.");
|
||||
|
||||
log.info("Successfully listed {} monitor types", monitorTypes);
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to list monitor types: {}", e.getMessage(), e);
|
||||
return "Error retrieving monitor types: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "get_monitor_additional_params", description = """
|
||||
Get the parameter definitions required for a specific monitor type.
|
||||
This tool shows what parameters are needed when adding a monitor of the specified type,
|
||||
ALWAYS use this before adding a monitor to understand what parameters the user needs to provide.
|
||||
Use the app parameter to specify the monitor type/application name (e.g., 'linux', 'mysql', 'redis') this can be obtained from the list_monitor_types tool.
|
||||
""")
|
||||
public String getMonitorAdditionalParams(
|
||||
@ToolParam(description = "Monitor type/application name (e.g., 'linux', 'mysql', 'redis')", required = true) String app) {
|
||||
|
||||
try {
|
||||
log.info("Getting parameter definitions for monitor type: {}", app);
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current subject in get_monitor_param_defines tool: {}", subjectSum);
|
||||
|
||||
// Validate required parameter
|
||||
if (app == null || app.trim().isEmpty()) {
|
||||
return "Error: Monitor type/application parameter is required";
|
||||
}
|
||||
|
||||
// Get parameter definitions from adapter
|
||||
List<ParamDefine> paramDefines = monitorServiceAdapter.getMonitorParamDefines(app);
|
||||
|
||||
if (paramDefines == null || paramDefines.isEmpty()) {
|
||||
return String.format("No parameter definitions found for monitor type '%s'. "
|
||||
+ "This monitor type may not exist or may not require additional parameters.", app);
|
||||
}
|
||||
|
||||
// Format the response
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append(String.format("Parameter Definitions for Monitor Type '%s' (Total: %d):\n\n",
|
||||
app, paramDefines.size()));
|
||||
|
||||
for (ParamDefine paramDefine : paramDefines) {
|
||||
response.append("• Field: ").append(paramDefine.getField()).append("\n");
|
||||
|
||||
// Add display name if available
|
||||
if (paramDefine.getName() != null && !paramDefine.getName().toString().trim().isEmpty()) {
|
||||
response.append(" Name: ").append(paramDefine.getName()).append("\n");
|
||||
}
|
||||
|
||||
// Add type
|
||||
if (paramDefine.getType() != null && !paramDefine.getType().trim().isEmpty()) {
|
||||
response.append(" Type: ").append(paramDefine.getType()).append("\n");
|
||||
}
|
||||
|
||||
// Add required status
|
||||
response.append(" Required: ").append(paramDefine.isRequired() ? "Yes" : "No").append("\n");
|
||||
|
||||
// Add default value if present
|
||||
if (paramDefine.getDefaultValue() != null && !paramDefine.getDefaultValue().trim().isEmpty()) {
|
||||
response.append(" Default: ").append(paramDefine.getDefaultValue()).append("\n");
|
||||
}
|
||||
|
||||
// Add validation range if present
|
||||
if (paramDefine.getRange() != null && !paramDefine.getRange().trim().isEmpty()) {
|
||||
response.append(" Range: ").append(paramDefine.getRange()).append("\n");
|
||||
}
|
||||
|
||||
// Add limit if present
|
||||
if (paramDefine.getLimit() != null) {
|
||||
response.append(" Limit: ").append(paramDefine.getLimit()).append("\n");
|
||||
}
|
||||
|
||||
// Add placeholder text if present
|
||||
if (paramDefine.getPlaceholder() != null && !paramDefine.getPlaceholder().trim().isEmpty()) {
|
||||
response.append(" Placeholder: ").append(paramDefine.getPlaceholder()).append("\n");
|
||||
}
|
||||
|
||||
response.append("\n");
|
||||
}
|
||||
|
||||
response.append("To add a monitor of this type, use the add_monitor tool with these parameters.\n");
|
||||
response.append(String.format("Example: add_monitor(name='my-monitor', app='%s', host='your-host', ...)", app));
|
||||
|
||||
log.info("Successfully retrieved {} parameter definitions for monitor type: {}", paramDefines.size(), app);
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get parameter definitions for monitor type '{}': {}", app, e.getMessage(), e);
|
||||
return "Error retrieving parameter definitions for monitor type '" + app + "': " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
+691
@@ -0,0 +1,691 @@
|
||||
/*
|
||||
* 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.ai.agent.utils;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.Hierarchy;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Utility class providing helper methods for field expression validation, parsing,
|
||||
* hierarchy management, timestamp formatting and metric/status conversions.
|
||||
* This class contains stateless utility methods used across the application for
|
||||
* common operations and validations.
|
||||
*/
|
||||
@Slf4j
|
||||
@lombok.experimental.UtilityClass
|
||||
public class UtilityClass {
|
||||
|
||||
/**
|
||||
* Validates the syntax of field conditions expression
|
||||
* @param fieldConditions Field conditions string to validate
|
||||
* @return "VALID" if syntax is correct, error message otherwise
|
||||
*/
|
||||
|
||||
public String validateExpressionSyntax(String fieldConditions) {
|
||||
try {
|
||||
log.debug("Validating expression syntax: {}", fieldConditions);
|
||||
|
||||
// Check for basic syntax requirements
|
||||
if (fieldConditions == null || fieldConditions.trim().isEmpty()) {
|
||||
return "Error: Field conditions cannot be empty";
|
||||
}
|
||||
|
||||
// Check for balanced parentheses
|
||||
if (!hasBalancedParentheses(fieldConditions)) {
|
||||
return "Error: Unbalanced parentheses in field conditions. Please check your expression syntax.";
|
||||
}
|
||||
|
||||
// Validate operators used in the expression
|
||||
String operatorValidation = validateOperators(fieldConditions);
|
||||
if (!operatorValidation.equals("VALID")) {
|
||||
return operatorValidation;
|
||||
}
|
||||
|
||||
// Validate logical connectors
|
||||
String logicalValidation = validateLogicalConnectors(fieldConditions);
|
||||
if (!logicalValidation.equals("VALID")) {
|
||||
return logicalValidation;
|
||||
}
|
||||
|
||||
// Validate function syntax (equals, contains, etc.)
|
||||
String functionValidation = validateFunctions(fieldConditions);
|
||||
if (!functionValidation.equals("VALID")) {
|
||||
return functionValidation;
|
||||
}
|
||||
|
||||
log.debug("Expression syntax validation passed for: {}", fieldConditions);
|
||||
return "VALID";
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error during expression syntax validation: {}", e.getMessage(), e);
|
||||
return String.format("Error: Unable to validate expression syntax: %s", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if parentheses are balanced in the expression
|
||||
*/
|
||||
public boolean hasBalancedParentheses(String expression) {
|
||||
int count = 0;
|
||||
for (char c : expression.toCharArray()) {
|
||||
if (c == '(') {
|
||||
count++;
|
||||
} else if (c == ')') {
|
||||
count--;
|
||||
if (count < 0) {
|
||||
return false; // More closing than opening
|
||||
}
|
||||
}
|
||||
}
|
||||
return count == 0; // Should be perfectly balanced
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that only supported operators are used
|
||||
*/
|
||||
public String validateOperators(String fieldConditions) {
|
||||
// Define supported operators for different field types
|
||||
String[] numericOperators = {">", "<", ">=", "<=", "==", "!=", "exists()", "!exists()"};
|
||||
String[] stringOperators = {"equals(", "contains(", "matches(", "exists()", "!equals(", "!contains(", "!matches(", "!exists()"};
|
||||
String[] logicalOperators = {" and ", " or "};
|
||||
|
||||
// Remove quotes and function calls temporarily for operator checking
|
||||
String tempExpression = fieldConditions
|
||||
.replaceAll("\"[^\"]*\"", "VALUE") // Remove quoted strings
|
||||
.replaceAll("'[^']*'", "VALUE") // Remove single quoted strings
|
||||
.replaceAll("\\w+\\([^)]*\\)", "FUNCTION"); // Remove function calls
|
||||
|
||||
// Check for invalid operators (common mistakes)
|
||||
String[] invalidOperators = {"&&", "||", "AND", "OR", "=", "!="};
|
||||
for (String invalidOp : invalidOperators) {
|
||||
if (tempExpression.contains(invalidOp)) {
|
||||
if (invalidOp.equals("&&") || invalidOp.equals("||")) {
|
||||
return String.format("Error: Use 'and'/'or' instead of '%s' for logical operations", invalidOp);
|
||||
}
|
||||
if (invalidOp.equals("AND") || invalidOp.equals("OR")) {
|
||||
return String.format("Error: Use lowercase '%s' for logical operations", invalidOp.toLowerCase());
|
||||
}
|
||||
if (invalidOp.equals("=")) {
|
||||
return "Error: Use '==' for equality comparison, not '='";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for unsupported special characters that might indicate syntax errors
|
||||
if (tempExpression.matches(".*[#$%^&*+\\[\\]{}|\\\\;:'\"`~].*")) {
|
||||
return "Error: Expression contains unsupported special characters. Use only supported operators and functions.";
|
||||
}
|
||||
|
||||
return "VALID";
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates logical connectors syntax
|
||||
*/
|
||||
public String validateLogicalConnectors(String fieldConditions) {
|
||||
// Check for proper spacing around logical operators
|
||||
if (fieldConditions.matches(".*(\\S(and|or)\\S).*")) {
|
||||
return "Error: Logical operators 'and'/'or' must be surrounded by spaces";
|
||||
}
|
||||
|
||||
// Check for consecutive logical operators
|
||||
if (fieldConditions.matches(".*(and\\s+and|or\\s+or|and\\s+or\\s+and|or\\s+and\\s+or).*")) {
|
||||
return "Error: Consecutive logical operators found. Use parentheses to group conditions properly.";
|
||||
}
|
||||
|
||||
// Check for logical operators at the beginning or end
|
||||
String trimmed = fieldConditions.trim();
|
||||
if (trimmed.startsWith("and ") || trimmed.startsWith("or ")
|
||||
|| trimmed.endsWith(" and") || trimmed.endsWith(" or")) {
|
||||
return "Error: Expression cannot start or end with logical operators 'and'/'or'";
|
||||
}
|
||||
|
||||
return "VALID";
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates function syntax (equals, contains, matches, etc.)
|
||||
*/
|
||||
public String validateFunctions(String fieldConditions) {
|
||||
// Check for properly formed function calls
|
||||
String[] supportedFunctions = {"equals", "contains", "matches", "exists", "!equals", "!contains", "!matches", "!exists"};
|
||||
|
||||
// Find all function-like patterns
|
||||
java.util.regex.Pattern functionPattern = java.util.regex.Pattern.compile("(!?\\w+)\\s*\\(([^)]*)\\)");
|
||||
java.util.regex.Matcher matcher = functionPattern.matcher(fieldConditions);
|
||||
|
||||
while (matcher.find()) {
|
||||
String functionName = matcher.group(1);
|
||||
String functionArgs = matcher.group(2);
|
||||
|
||||
// Check if function is supported
|
||||
boolean isSupported = false;
|
||||
for (String supportedFunc : supportedFunctions) {
|
||||
if (functionName.equals(supportedFunc)) {
|
||||
isSupported = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSupported) {
|
||||
return String.format("Error: Unsupported function '%s'. Supported functions: %s",
|
||||
functionName, String.join(", ", supportedFunctions));
|
||||
}
|
||||
|
||||
// Validate function arguments
|
||||
if (functionName.equals("exists") || functionName.equals("!exists")) {
|
||||
// exists() should have one parameter or no parameters
|
||||
String[] args = functionArgs.trim().isEmpty() ? new String[0] : functionArgs.split(",");
|
||||
if (args.length > 1) {
|
||||
return String.format("Error: Function '%s' should have at most one parameter", functionName);
|
||||
}
|
||||
} else {
|
||||
// Other functions should have exactly 2 parameters
|
||||
String[] args = functionArgs.split(",");
|
||||
if (args.length != 2) {
|
||||
return String.format("Error: Function '%s' requires exactly 2 parameters (field, value)", functionName);
|
||||
}
|
||||
|
||||
// Check that parameters are not empty
|
||||
for (String arg : args) {
|
||||
if (arg.trim().isEmpty()) {
|
||||
return String.format("Error: Function '%s' has empty parameter", functionName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "VALID";
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to validate operator
|
||||
*/
|
||||
public boolean isValidOperator(String operator) {
|
||||
return operator != null && (operator.equals(">") || operator.equals("<")
|
||||
|| operator.equals(">=") || operator.equals("<=")
|
||||
|| operator.equals("==") || operator.equals("!="));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to validate priority
|
||||
*/
|
||||
public boolean isValidPriority(String priority) {
|
||||
return priority != null && (priority.equalsIgnoreCase("critical")
|
||||
|| priority.equalsIgnoreCase("warning") || priority.equalsIgnoreCase("info"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to build expression
|
||||
*/
|
||||
public String buildExpression(String metric, String operator, String threshold) {
|
||||
return String.format("%s %s %s", metric, operator, threshold);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to parse existing expression into components
|
||||
*/
|
||||
public String[] parseExpression(String expression) {
|
||||
if (expression == null || expression.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Simple parsing for basic expressions like "metric > value"
|
||||
String[] operators = {">", "<", ">=", "<=", "==", "!="};
|
||||
for (String op : operators) {
|
||||
if (expression.contains(" " + op + " ")) {
|
||||
String[] parts = expression.split(" " + op + " ");
|
||||
if (parts.length == 2) {
|
||||
return new String[]{parts[0].trim(), op, parts[1].trim()};
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to parse key-value pairs from a string
|
||||
* Format: "key1:value1, key2:value2, ..."
|
||||
*/
|
||||
public Map<String, String> parseKeyValuePairs(String input) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
if (input == null || input.trim().isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
String[] pairs = input.split(",");
|
||||
for (String pair : pairs) {
|
||||
String[] keyValue = pair.split(":");
|
||||
if (keyValue.length == 2) {
|
||||
result.put(keyValue[0].trim(), keyValue[1].trim());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Recursively searches for a metric in the hierarchy
|
||||
*/
|
||||
public Hierarchy findMetricInHierarchy(List<Hierarchy> hierarchies, String metricName) {
|
||||
for (Hierarchy hierarchy : hierarchies) {
|
||||
// Check if this is the metric we're looking for
|
||||
if (metricName.equals(hierarchy.getValue())) {
|
||||
// Verify it has field children (leaf nodes)
|
||||
if (hierarchy.getChildren() != null && !hierarchy.getChildren().isEmpty()) {
|
||||
boolean hasLeafChildren = hierarchy.getChildren().stream()
|
||||
.anyMatch(child -> child.getIsLeaf() != null && child.getIsLeaf());
|
||||
if (hasLeafChildren) {
|
||||
return hierarchy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively search in children
|
||||
if (hierarchy.getChildren() != null) {
|
||||
Hierarchy found = findMetricInHierarchy(hierarchy.getChildren(), metricName);
|
||||
if (found != null) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a field is valid for the given metric
|
||||
*/
|
||||
public boolean isFieldValidForMetric(Hierarchy metricHierarchy, String fieldName) {
|
||||
if (metricHierarchy.getChildren() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Hierarchy child : metricHierarchy.getChildren()) {
|
||||
if (child.getIsLeaf() != null && child.getIsLeaf() && fieldName.equals(child.getValue())) {
|
||||
return true;
|
||||
}
|
||||
// Also check nested children
|
||||
if (child.getChildren() != null && isFieldValidForMetric(child, fieldName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts field names from field conditions string
|
||||
* Handles simple cases like "field > 80", "equals(field, 'value')", complex expressions
|
||||
*/
|
||||
public List<String> extractFieldNamesFromConditions(String fieldConditions) {
|
||||
List<String> fieldNames = new ArrayList<>();
|
||||
|
||||
// Split by logical operators (and, or) and parentheses, but preserve the field names
|
||||
// This is a simple implementation - could be enhanced with a proper parser
|
||||
String[] parts = fieldConditions.split("\\s+(and|or|&&|\\|\\|)\\s+|[()]+");
|
||||
|
||||
for (String part : parts) {
|
||||
part = part.trim();
|
||||
if (part.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle equals() function: equals(fieldName, "value")
|
||||
if (part.contains("equals(")) {
|
||||
String fieldName = extractFieldFromEquals(part);
|
||||
if (fieldName != null && !fieldNames.contains(fieldName)) {
|
||||
fieldNames.add(fieldName);
|
||||
}
|
||||
} else {
|
||||
// Handle simple comparisons: fieldName > value, fieldName <= value
|
||||
String fieldName = extractFieldFromComparison(part);
|
||||
if (fieldName != null && !fieldNames.contains(fieldName)) {
|
||||
fieldNames.add(fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fieldNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts field name from equals() function
|
||||
*/
|
||||
public String extractFieldFromEquals(String condition) {
|
||||
// Pattern: equals(fieldName, "value") or equals(fieldName, value)
|
||||
int startParen = condition.indexOf('(');
|
||||
int comma = condition.indexOf(',');
|
||||
|
||||
if (startParen != -1 && comma != -1 && comma > startParen) {
|
||||
String fieldName = condition.substring(startParen + 1, comma).trim();
|
||||
// Remove quotes if present
|
||||
if (fieldName.startsWith("\"") && fieldName.endsWith("\"")) {
|
||||
fieldName = fieldName.substring(1, fieldName.length() - 1);
|
||||
}
|
||||
return fieldName;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts field name from comparison operation
|
||||
*/
|
||||
public String extractFieldFromComparison(String condition) {
|
||||
// Pattern: fieldName operator value
|
||||
// Updated to include all supported operators
|
||||
String[] operators = {" >= ", " <= ", " > ", " < ", " == ", " != "};
|
||||
|
||||
for (String operator : operators) {
|
||||
if (condition.contains(operator)) {
|
||||
String fieldName = condition.split(operator)[0].trim();
|
||||
// Basic validation - field names shouldn't contain quotes or special chars
|
||||
if (fieldName.matches("[a-zA-Z_][a-zA-Z0-9_]*")) {
|
||||
return fieldName;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to format hierarchy structure as JSON recursively
|
||||
*/
|
||||
public ObjectNode formatHierarchyAsJson(ObjectMapper mapper, Hierarchy hierarchy) {
|
||||
ObjectNode node = mapper.createObjectNode();
|
||||
|
||||
node.put("value", hierarchy.getValue());
|
||||
node.put("label", hierarchy.getLabel());
|
||||
|
||||
if (hierarchy.getIsLeaf() != null && hierarchy.getIsLeaf()) {
|
||||
// Leaf node - actual metric field parameter
|
||||
node.put("type", "field_parameter");
|
||||
|
||||
if (hierarchy.getType() != null) {
|
||||
node.put("dataType", hierarchy.getType() == 0 ? "numeric" : "string");
|
||||
}
|
||||
if (hierarchy.getUnit() != null && !hierarchy.getUnit().trim().isEmpty()) {
|
||||
node.put("unit", hierarchy.getUnit());
|
||||
}
|
||||
node.put("description", "Available field parameter for alert conditions");
|
||||
} else {
|
||||
// Category, app, or metric node
|
||||
// Determine node type based on children
|
||||
boolean hasLeafChildren = hierarchy.getChildren().stream()
|
||||
.anyMatch(child -> child.getIsLeaf() != null && child.getIsLeaf());
|
||||
|
||||
if (hasLeafChildren) {
|
||||
node.put("type", "metric");
|
||||
node.put("description", "Metric with available field parameters");
|
||||
} else {
|
||||
node.put("type", "app");
|
||||
node.put("description", "Application with available metrics");
|
||||
}
|
||||
|
||||
if (hierarchy.getChildren() != null && !hierarchy.getChildren().isEmpty()) {
|
||||
ArrayNode childrenArray = mapper.createArrayNode();
|
||||
for (Hierarchy child : hierarchy.getChildren()) {
|
||||
childrenArray.add(formatHierarchyAsJson(mapper, child));
|
||||
}
|
||||
node.set("children", childrenArray);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format timestamp to readable format
|
||||
*/
|
||||
public String formatTimestamp(Long timestamp) {
|
||||
if (timestamp == null) {
|
||||
return "N/A";
|
||||
}
|
||||
LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault());
|
||||
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse time range string to milliseconds
|
||||
*/
|
||||
public long parseTimeRangeToMillis(String timeRange) {
|
||||
return switch (timeRange.toLowerCase()) {
|
||||
case "1h" -> 60 * 60 * 1000L;
|
||||
case "6h" -> 6 * 60 * 60 * 1000L;
|
||||
case "24h" -> 24 * 60 * 60 * 1000L;
|
||||
case "7d" -> 7 * 24 * 60 * 60 * 1000L;
|
||||
default -> 24 * 60 * 60 * 1000L; // default to 24h
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to convert monitor status byte to readable text
|
||||
* @param status The status byte from monitor
|
||||
* @return Human-readable status text
|
||||
*/
|
||||
public String getStatusText(Byte status) {
|
||||
if (status == null) {
|
||||
return "Unknown";
|
||||
}
|
||||
return switch (status) {
|
||||
case 0 -> "Paused";
|
||||
case 1 -> "Online";
|
||||
case 2 -> "Offline";
|
||||
case 3 -> "Unreachable";
|
||||
default -> "Unknown (" + status + ")";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get metrics name for a metric type
|
||||
*/
|
||||
public String getMetricsNameForType(String metricType) {
|
||||
return switch (metricType.toLowerCase()) {
|
||||
case "cpu" -> "cpu";
|
||||
case "memory" -> "memory";
|
||||
case "disk" -> "disk";
|
||||
case "network" -> "network";
|
||||
default -> "system";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to check if a field represents usage for a metric type
|
||||
*/
|
||||
public boolean isUsageField(String field, String metricType) {
|
||||
if (field == null) return false;
|
||||
|
||||
String fieldLower = field.toLowerCase();
|
||||
String typeLower = metricType.toLowerCase();
|
||||
|
||||
return fieldLower.contains("usage")
|
||||
|| fieldLower.contains("percent")
|
||||
|| fieldLower.contains("util")
|
||||
|| (typeLower.equals("cpu") && (fieldLower.contains("cpu") || fieldLower.contains("idle")))
|
||||
|| (typeLower.equals("memory") && fieldLower.contains("memory"))
|
||||
|| (typeLower.equals("disk") && fieldLower.contains("disk"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert manager module Hierarchy objects to ai-agent module Hierarchy objects
|
||||
* This handles the cross-module DTO conversion to avoid ClassCastException
|
||||
*/
|
||||
public List<Hierarchy> convertToAgentHierarchies(List<?> managerHierarchies) {
|
||||
List<Hierarchy> agentHierarchies = new ArrayList<>();
|
||||
|
||||
for (Object managerHierarchy : managerHierarchies) {
|
||||
Hierarchy agentHierarchy = convertToAgentHierarchy(managerHierarchy);
|
||||
agentHierarchies.add(agentHierarchy);
|
||||
}
|
||||
|
||||
return agentHierarchies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a single manager Hierarchy object to ai-agent Hierarchy object using reflection
|
||||
*/
|
||||
public Hierarchy convertToAgentHierarchy(Object managerHierarchy) {
|
||||
try {
|
||||
Hierarchy agentHierarchy = new Hierarchy();
|
||||
|
||||
// Use reflection to copy properties from manager DTO to agent DTO
|
||||
Class<?> managerClass = managerHierarchy.getClass();
|
||||
|
||||
// Copy basic properties
|
||||
agentHierarchy.setCategory(getStringField(managerHierarchy, managerClass, "category"));
|
||||
agentHierarchy.setValue(getStringField(managerHierarchy, managerClass, "value"));
|
||||
agentHierarchy.setLabel(getStringField(managerHierarchy, managerClass, "label"));
|
||||
agentHierarchy.setIsLeaf(getBooleanField(managerHierarchy, managerClass, "isLeaf"));
|
||||
agentHierarchy.setHide(getBooleanField(managerHierarchy, managerClass, "hide"));
|
||||
agentHierarchy.setType(getByteField(managerHierarchy, managerClass, "type"));
|
||||
agentHierarchy.setUnit(getStringField(managerHierarchy, managerClass, "unit"));
|
||||
|
||||
// Handle children recursively
|
||||
List<?> managerChildren = getListField(managerHierarchy, managerClass, "children");
|
||||
if (managerChildren != null && !managerChildren.isEmpty()) {
|
||||
List<Hierarchy> agentChildren = convertToAgentHierarchies(managerChildren);
|
||||
agentHierarchy.setChildren(agentChildren);
|
||||
}
|
||||
|
||||
return agentHierarchy;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to convert manager hierarchy to agent hierarchy: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("Failed to convert hierarchy", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getStringField(Object obj, Class<?> clazz, String fieldName) {
|
||||
try {
|
||||
Method getter = clazz.getMethod("get" + capitalize(fieldName));
|
||||
Object value = getter.invoke(obj);
|
||||
return value != null ? value.toString() : null;
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not get string field '{}': {}", fieldName, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Boolean getBooleanField(Object obj, Class<?> clazz, String fieldName) {
|
||||
try {
|
||||
Method getter = clazz.getMethod("get" + capitalize(fieldName));
|
||||
Object value = getter.invoke(obj);
|
||||
return value instanceof Boolean ? (Boolean) value : null;
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
// Try alternative getter pattern for boolean fields
|
||||
Method isGetter = clazz.getMethod("is" + capitalize(fieldName));
|
||||
Object value = isGetter.invoke(obj);
|
||||
return value instanceof Boolean ? (Boolean) value : null;
|
||||
} catch (Exception e2) {
|
||||
log.debug("Could not get boolean field '{}': {}", fieldName, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Byte getByteField(Object obj, Class<?> clazz, String fieldName) {
|
||||
try {
|
||||
Method getter = clazz.getMethod("get" + capitalize(fieldName));
|
||||
Object value = getter.invoke(obj);
|
||||
return value instanceof Byte ? (Byte) value : null;
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not get byte field '{}': {}", fieldName, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public List<?> getListField(Object obj, Class<?> clazz, String fieldName) {
|
||||
try {
|
||||
Method getter = clazz.getMethod("get" + capitalize(fieldName));
|
||||
Object value = getter.invoke(obj);
|
||||
return value instanceof List ? (List<?>) value : null;
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not get list field '{}': {}", fieldName, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String capitalize(String str) {
|
||||
if (str == null || str.isEmpty()) {
|
||||
return str;
|
||||
}
|
||||
return str.substring(0, 1).toUpperCase() + str.substring(1);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extract existing monitor IDs from the alert expression
|
||||
* @param expression The alert expression containing __instance__ conditions
|
||||
* @return List of existing monitor IDs
|
||||
*/
|
||||
public List<String> extractExistingMonitorIds(String expression) {
|
||||
List<String> monitorIds = new ArrayList<>();
|
||||
String pattern = "equals\\(__instance__,\\s*\"([^\"]+)\"\\)";
|
||||
java.util.regex.Pattern regex = java.util.regex.Pattern.compile(pattern);
|
||||
java.util.regex.Matcher matcher = regex.matcher(expression);
|
||||
|
||||
while (matcher.find()) {
|
||||
monitorIds.add(matcher.group(1));
|
||||
}
|
||||
|
||||
return monitorIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace existing __instance__ conditions in the expression with new ones
|
||||
* @param expression The current expression
|
||||
* @param newMonitorCondition The new monitor condition to replace with
|
||||
* @return Updated expression
|
||||
*/
|
||||
public String replaceInstanceConditions(String expression, String newMonitorCondition) {
|
||||
// More precise pattern to match complete __instance__ condition blocks without cutting field parameters
|
||||
// Pattern matches either:
|
||||
// 1. Single: equals(__instance__, "id")
|
||||
// 2. Multiple: (equals(__instance__, "id1") or equals(__instance__, "id2") or ...)
|
||||
|
||||
// First try to match grouped conditions: (equals(__instance__, "id1") or equals(__instance__, "id2"))
|
||||
String groupedPattern = "\\(\\s*equals\\(__instance__,\\s*\"[^\"]+\"\\)(?:\\s+or\\s+equals\\(__instance__,\\s*\"[^\"]+\"\\))*\\s*\\)";
|
||||
if (expression.matches(".*" + groupedPattern + ".*")) {
|
||||
return expression.replaceFirst(groupedPattern, newMonitorCondition);
|
||||
}
|
||||
|
||||
// Then try single condition: equals(__instance__, "id")
|
||||
String singlePattern = "equals\\(__instance__,\\s*\"[^\"]+\"\\)";
|
||||
if (expression.matches(".*" + singlePattern + ".*")) {
|
||||
return expression.replaceFirst(singlePattern, newMonitorCondition);
|
||||
}
|
||||
|
||||
// If no match found, return original expression
|
||||
return expression;
|
||||
}
|
||||
|
||||
}
|
||||
+14
-4
@@ -22,10 +22,12 @@ package org.apache.hertzbeat.alert.config;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
@@ -54,16 +56,24 @@ public class AlertSseManager {
|
||||
.name("ALERT_EVENT")
|
||||
.data(data));
|
||||
} catch (IOException | IllegalStateException e) {
|
||||
emitter.complete();
|
||||
removeEmitter(clientId);
|
||||
tryCompleteAndClean(clientId, emitter);
|
||||
} catch (Exception exception) {
|
||||
log.error("Failed to broadcast alert data to client: {}", exception.getMessage());
|
||||
emitter.complete();
|
||||
removeEmitter(clientId);
|
||||
tryCompleteAndClean(clientId, emitter);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void tryCompleteAndClean(Long clientId, SseEmitter emitter) {
|
||||
try {
|
||||
Optional.ofNullable(emitter).ifPresent(ResponseBodyEmitter::complete);
|
||||
} catch (Throwable e) {
|
||||
log.debug("Failed to complete emitter for client {}: {}", clientId, e.getMessage());
|
||||
}
|
||||
// execute clear
|
||||
removeEmitter(clientId);
|
||||
}
|
||||
|
||||
private void removeEmitter(Long clientId) {
|
||||
emitters.remove(clientId);
|
||||
}
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ public class ServerChanAlertNotifyHandlerImpl extends AbstractAlertNotifyHandler
|
||||
String webHookUrl = String.format(alerterProperties.getServerChanWebhookUrl(), sanitizedToken);
|
||||
|
||||
// Validate the constructed URL against a whitelist
|
||||
List<String> allowedBaseUrls = List.of("https://api.serverchan.com", "https://serverchan.example.com");
|
||||
List<String> allowedBaseUrls = List.of("https://api.serverchan.com", "https://serverchan.example.com", "https://sctapi.ftqq.com");
|
||||
boolean isValidUrl = allowedBaseUrls.stream().anyMatch(webHookUrl::startsWith);
|
||||
if (!isValidUrl) {
|
||||
throw new AlertNoticeException("Invalid webhook URL: " + webHookUrl);
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.alert.config;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* alert sse manager test
|
||||
*/
|
||||
public class AlertSseManagerTest {
|
||||
|
||||
private AlertSseManager alertSseManager;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
alertSseManager = new AlertSseManager();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCompleteThrowsException() throws Exception {
|
||||
SseEmitter emitter = alertSseManager.createEmitter(1L);
|
||||
assertNotNull(emitter);
|
||||
|
||||
Map<Long, SseEmitter> emitters = new HashMap<>();
|
||||
SseEmitter spyEmitter = mock(SseEmitter.class);
|
||||
|
||||
doThrow(new IllegalStateException("Simulated output stream error")).when(spyEmitter).send(any(SseEmitter.SseEventBuilder.class));
|
||||
doThrow(new RuntimeException("Complete failed")).when(spyEmitter).complete();
|
||||
|
||||
emitters.put(1L, spyEmitter);
|
||||
|
||||
Field emittersField = AlertSseManager.class.getDeclaredField("emitters");
|
||||
emittersField.setAccessible(true);
|
||||
emittersField.set(alertSseManager, emitters);
|
||||
|
||||
assertThrows(RuntimeException.class, () -> alertSseManager.broadcast("{\"id\":1,\"content\":\"Test alert\"}"));
|
||||
Map<Long, SseEmitter> currentEmitters = (Map<Long, SseEmitter>) emittersField.get(alertSseManager);
|
||||
assertFalse(currentEmitters.containsKey(1L), "Emitter should still exist because complete() threw exception");
|
||||
}
|
||||
|
||||
}
|
||||
+23
-4
@@ -69,6 +69,10 @@ public class OnlineParser {
|
||||
parseMetric(inputStream, metricFamilyMap, stringBuilder);
|
||||
}
|
||||
i = getChar(inputStream);
|
||||
// To address the `\n\r` scenario, it is necessary to skip
|
||||
if (i == '\r') {
|
||||
i = getChar(inputStream);
|
||||
}
|
||||
}
|
||||
} catch (FormatException e) {
|
||||
log.error("prometheus parser failed because of wrong input format. {}", e.getMessage());
|
||||
@@ -185,6 +189,10 @@ public class OnlineParser {
|
||||
stringBuilder.append((char) i);
|
||||
i = getChar(inputStream);
|
||||
}
|
||||
// Skip \r character to handle Windows line endings
|
||||
if (i == '\r') {
|
||||
i = getChar(inputStream);
|
||||
}
|
||||
return new CharChecker(i);
|
||||
}
|
||||
|
||||
@@ -193,6 +201,10 @@ public class OnlineParser {
|
||||
while (i >= '0' && i <= '9') {
|
||||
i = getChar(inputStream);
|
||||
}
|
||||
// Skip \r character to handle Windows line endings
|
||||
if (i == '\r') {
|
||||
i = getChar(inputStream);
|
||||
}
|
||||
return new CharChecker(i);
|
||||
}
|
||||
|
||||
@@ -215,20 +227,27 @@ public class OnlineParser {
|
||||
}
|
||||
|
||||
private static CharChecker parseLabelValue(InputStream inputStream, StringBuilder stringBuilder) throws IOException, FormatException {
|
||||
int i = getChar(inputStream);
|
||||
int i = inputStream.read();
|
||||
while (i != '"' && i != -1) {
|
||||
if (i == '\\') {
|
||||
i = getChar(inputStream);
|
||||
i = inputStream.read();
|
||||
switch (i) {
|
||||
case 'n' -> stringBuilder.append('\n');
|
||||
case '\\' -> stringBuilder.append('\\');
|
||||
case '\"' -> stringBuilder.append('\"');
|
||||
default -> throw new FormatException();
|
||||
default -> {
|
||||
// Unknown escape, keep as-is
|
||||
// https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/lib/protoparser/prometheus/parser.go#L419
|
||||
stringBuilder.append('\\');
|
||||
if (i != -1) {
|
||||
stringBuilder.append((char) i);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stringBuilder.append((char) i);
|
||||
}
|
||||
i = getChar(inputStream);
|
||||
i = inputStream.read();
|
||||
}
|
||||
return new CharChecker(i);
|
||||
}
|
||||
|
||||
+354
@@ -43,6 +43,16 @@ class OnlineParserTest {
|
||||
assertNotNull(metricFamilyMap);
|
||||
}
|
||||
|
||||
@Disabled // because unless you have already saved the locally tested files
|
||||
@Test
|
||||
void parseTestFile() throws Exception {
|
||||
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("test_file.txt");
|
||||
assertNotNull(inputStream, "Failed to load test_file.txt resource");
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
assertNotNull(metricFamilyMap);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void parseMetrics2() throws Exception {
|
||||
String str = """
|
||||
@@ -105,4 +115,348 @@ class OnlineParserTest {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricsWithCrLf() throws Exception {
|
||||
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\r\n"
|
||||
+ "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0\r\n"
|
||||
+ "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139\r\n";
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(2, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
|
||||
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
|
||||
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
|
||||
|
||||
str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\r\n"
|
||||
+ "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0 1234567890\r\n"
|
||||
+ "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139 1234567890\r\n";
|
||||
inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(2, metricFamilyMap.values().size());
|
||||
|
||||
metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
|
||||
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
|
||||
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricsWithLf() throws Exception {
|
||||
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\n"
|
||||
+ "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0\n"
|
||||
+ "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139\n";
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(2, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
|
||||
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
|
||||
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
|
||||
|
||||
str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\n"
|
||||
+ "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0 1234567890\n"
|
||||
+ "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139 1234567890\n";
|
||||
inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(2, metricFamilyMap.values().size());
|
||||
|
||||
metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
|
||||
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
|
||||
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricsWithoutFinalNewline() throws Exception {
|
||||
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\r\n"
|
||||
+ "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0\r\n"
|
||||
+ "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139";
|
||||
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(2, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
|
||||
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
|
||||
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
|
||||
|
||||
str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\n"
|
||||
+ "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0\n"
|
||||
+ "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139";
|
||||
|
||||
inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(2, metricFamilyMap.values().size());
|
||||
|
||||
metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
|
||||
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
|
||||
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricsWithEmptyLabelsAndCrLf() throws Exception {
|
||||
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\r\n"
|
||||
+ "jvm_gc_pause_seconds_count 1.0\r\n"
|
||||
+ "jvm_gc_pause_seconds_sum{} 0.139";
|
||||
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(2, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
|
||||
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
|
||||
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricsWithEmptyLabelsAndLf() throws Exception {
|
||||
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\n"
|
||||
+ "jvm_gc_pause_seconds_count 1.0\n"
|
||||
+ "jvm_gc_pause_seconds_sum{} 0.139";
|
||||
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(2, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
|
||||
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
|
||||
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricsWithMixedLineEndings() throws Exception {
|
||||
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\n"
|
||||
+ "jvm_gc_pause_seconds_count 1.0\n"
|
||||
+ "jvm_gc_pause_seconds_sum{} 0.139\r\n";
|
||||
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(2, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
|
||||
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
|
||||
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricsWithLfCrLineEnding() throws Exception {
|
||||
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n\r"
|
||||
+ "# TYPE go_gc_duration_seconds summary\n\r"
|
||||
+ "jvm_gc_pause_seconds_count 1.0\n\r"
|
||||
+ "jvm_gc_pause_seconds_sum{} 0.139\n\r";
|
||||
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(2, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
|
||||
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
|
||||
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricsWithMixedLineEndingsIncludingLfCr() throws Exception {
|
||||
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\n"
|
||||
+ "jvm_gc_pause_seconds_count 1.0\n\r"
|
||||
+ "jvm_gc_pause_seconds_sum{} 0.139\r\n"
|
||||
+ "jvm_gc_pause_seconds_max{} 0.139\n";
|
||||
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(3, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
|
||||
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
|
||||
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
|
||||
|
||||
MetricFamily metricFamily2 = metricFamilyMap.get("jvm_gc_pause_seconds_max");
|
||||
assertEquals("jvm_gc_pause_seconds_max", metricFamily2.getName());
|
||||
assertEquals(0.139, metricFamily2.getMetricList().get(0).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricsWithOnlyLfCr() throws Exception {
|
||||
String str = "jvm_gc_pause_seconds_sum 42.0\n\r";
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(1, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily.getName());
|
||||
assertEquals(42.0, metricFamily.getMetricList().get(0).getValue());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testParseMetricEscape1() throws Exception {
|
||||
// test escape '\\'
|
||||
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\n"
|
||||
+ "windows_service_info{display_name=\"\\\\Application Layer \nGateway Service\",name=\"alg\\\\\",process_id=\"0\",run_as=\"NT AUTHORITY\\\\LocalService\"} 1\n";
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(1, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("windows_service_info");
|
||||
assertEquals("windows_service_info", metricFamily.getName());
|
||||
assertNotNull(metricFamily.getMetricList().get(0).getLabels());
|
||||
assertEquals(4, metricFamily.getMetricList().get(0).getLabels().size());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
assertEquals("display_name", metricFamily.getMetricList().get(0).getLabels().get(0).getName());
|
||||
assertEquals("\\Application Layer \nGateway Service", metricFamily.getMetricList().get(0).getLabels().get(0).getValue());
|
||||
|
||||
assertEquals("name", metricFamily.getMetricList().get(0).getLabels().get(1).getName());
|
||||
assertEquals("alg\\", metricFamily.getMetricList().get(0).getLabels().get(1).getValue());
|
||||
|
||||
assertEquals("process_id", metricFamily.getMetricList().get(0).getLabels().get(2).getName());
|
||||
assertEquals("0", metricFamily.getMetricList().get(0).getLabels().get(2).getValue());
|
||||
|
||||
assertEquals("run_as", metricFamily.getMetricList().get(0).getLabels().get(3).getName());
|
||||
assertEquals("NT AUTHORITY\\LocalService", metricFamily.getMetricList().get(0).getLabels().get(3).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricEscape2() throws Exception {
|
||||
// test escape '\"'
|
||||
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\n"
|
||||
+ "windows_service_info{display_name=\"\\\"Application Layer \\\"Gateway Service\",name=\"alg\\\"\",process_id=\"0\",run_as=\"NT AUTHORITY\\\"LocalService\"} 1\n";
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(1, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("windows_service_info");
|
||||
assertEquals("windows_service_info", metricFamily.getName());
|
||||
assertNotNull(metricFamily.getMetricList().get(0).getLabels());
|
||||
assertEquals(4, metricFamily.getMetricList().get(0).getLabels().size());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
assertEquals("display_name", metricFamily.getMetricList().get(0).getLabels().get(0).getName());
|
||||
assertEquals("\"Application Layer \"Gateway Service", metricFamily.getMetricList().get(0).getLabels().get(0).getValue());
|
||||
|
||||
assertEquals("name", metricFamily.getMetricList().get(0).getLabels().get(1).getName());
|
||||
assertEquals("alg\"", metricFamily.getMetricList().get(0).getLabels().get(1).getValue());
|
||||
|
||||
assertEquals("process_id", metricFamily.getMetricList().get(0).getLabels().get(2).getName());
|
||||
assertEquals("0", metricFamily.getMetricList().get(0).getLabels().get(2).getValue());
|
||||
|
||||
assertEquals("run_as", metricFamily.getMetricList().get(0).getLabels().get(3).getName());
|
||||
assertEquals("NT AUTHORITY\"LocalService", metricFamily.getMetricList().get(0).getLabels().get(3).getValue());
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricEscape3() throws Exception {
|
||||
// test escape '\n'
|
||||
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\n"
|
||||
+ "windows_service_info{display_name=\"\nApplication Layer \nGateway Service\",name=\"alg\n\",process_id=\"0\",run_as=\"NT AUTHORITY\nLocalService\"} 1\n";
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(1, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("windows_service_info");
|
||||
assertEquals("windows_service_info", metricFamily.getName());
|
||||
assertNotNull(metricFamily.getMetricList().get(0).getLabels());
|
||||
assertEquals(4, metricFamily.getMetricList().get(0).getLabels().size());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
assertEquals("display_name", metricFamily.getMetricList().get(0).getLabels().get(0).getName());
|
||||
assertEquals("\nApplication Layer \nGateway Service", metricFamily.getMetricList().get(0).getLabels().get(0).getValue());
|
||||
|
||||
assertEquals("name", metricFamily.getMetricList().get(0).getLabels().get(1).getName());
|
||||
assertEquals("alg\n", metricFamily.getMetricList().get(0).getLabels().get(1).getValue());
|
||||
|
||||
assertEquals("process_id", metricFamily.getMetricList().get(0).getLabels().get(2).getName());
|
||||
assertEquals("0", metricFamily.getMetricList().get(0).getLabels().get(2).getValue());
|
||||
|
||||
assertEquals("run_as", metricFamily.getMetricList().get(0).getLabels().get(3).getName());
|
||||
assertEquals("NT AUTHORITY\nLocalService", metricFamily.getMetricList().get(0).getLabels().get(3).getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,4 +345,4 @@
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
</project>
|
||||
|
||||
@@ -83,13 +83,10 @@
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>apache-${project.artifactId}-${hzb.version}</finalName>
|
||||
</build>
|
||||
|
||||
|
||||
|
||||
</project>
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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.common.entity.manager;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
|
||||
/**
|
||||
* Metrics Favorite Entity
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "hzb_metrics_favorite",
|
||||
uniqueConstraints = @UniqueConstraint(columnNames = {"creator", "monitor_id", "metrics_name"}))
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class MetricsFavorite {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "Creator cannot be null or blank")
|
||||
@Size(max = 255, message = "Creator length cannot exceed 255 characters")
|
||||
@Column(name = "creator", nullable = false)
|
||||
private String creator;
|
||||
|
||||
@NotNull(message = "Monitor ID cannot be null")
|
||||
@Column(name = "monitor_id", nullable = false)
|
||||
private Long monitorId;
|
||||
|
||||
@NotBlank(message = "Metrics name cannot be null or blank")
|
||||
@Size(max = 255, message = "Metrics name length cannot exceed 255 characters")
|
||||
@Column(name = "metrics_name", nullable = false)
|
||||
private String metricsName;
|
||||
|
||||
@CreatedDate
|
||||
@Column(name = "create_time", updatable = false)
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
+323
@@ -0,0 +1,323 @@
|
||||
/*
|
||||
* 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.common.entity.manager;
|
||||
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.Validation;
|
||||
import jakarta.validation.Validator;
|
||||
import jakarta.validation.ValidatorFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Test case for {@link MetricsFavorite}
|
||||
*/
|
||||
class MetricsFavoriteTest {
|
||||
|
||||
private final ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
|
||||
private final Validator validator = factory.getValidator();
|
||||
|
||||
@Test
|
||||
void testBuilder() {
|
||||
String creator = "testUser";
|
||||
Long monitorId = 1L;
|
||||
String metricsName = "cpu";
|
||||
LocalDateTime createTime = LocalDateTime.now();
|
||||
|
||||
MetricsFavorite favorite = MetricsFavorite.builder()
|
||||
.id(1L)
|
||||
.creator(creator)
|
||||
.monitorId(monitorId)
|
||||
.metricsName(metricsName)
|
||||
.createTime(createTime)
|
||||
.build();
|
||||
|
||||
assertNotNull(favorite);
|
||||
assertEquals(1L, favorite.getId());
|
||||
assertEquals(creator, favorite.getCreator());
|
||||
assertEquals(monitorId, favorite.getMonitorId());
|
||||
assertEquals(metricsName, favorite.getMetricsName());
|
||||
assertEquals(createTime, favorite.getCreateTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBuilderWithoutOptionalFields() {
|
||||
String creator = "testUser";
|
||||
Long monitorId = 1L;
|
||||
String metricsName = "cpu";
|
||||
|
||||
MetricsFavorite favorite = MetricsFavorite.builder()
|
||||
.creator(creator)
|
||||
.monitorId(monitorId)
|
||||
.metricsName(metricsName)
|
||||
.build();
|
||||
|
||||
assertNotNull(favorite);
|
||||
assertNull(favorite.getId());
|
||||
assertEquals(creator, favorite.getCreator());
|
||||
assertEquals(monitorId, favorite.getMonitorId());
|
||||
assertEquals(metricsName, favorite.getMetricsName());
|
||||
assertNull(favorite.getCreateTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDefaultConstructor() {
|
||||
MetricsFavorite favorite = new MetricsFavorite();
|
||||
|
||||
assertNotNull(favorite);
|
||||
assertNull(favorite.getId());
|
||||
assertNull(favorite.getCreator());
|
||||
assertNull(favorite.getMonitorId());
|
||||
assertNull(favorite.getMetricsName());
|
||||
assertNull(favorite.getCreateTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSettersAndGetters() {
|
||||
MetricsFavorite favorite = new MetricsFavorite();
|
||||
String creator = "testUser";
|
||||
Long monitorId = 1L;
|
||||
String metricsName = "cpu";
|
||||
LocalDateTime createTime = LocalDateTime.now();
|
||||
|
||||
favorite.setId(1L);
|
||||
favorite.setCreator(creator);
|
||||
favorite.setMonitorId(monitorId);
|
||||
favorite.setMetricsName(metricsName);
|
||||
favorite.setCreateTime(createTime);
|
||||
|
||||
assertEquals(1L, favorite.getId());
|
||||
assertEquals(creator, favorite.getCreator());
|
||||
assertEquals(monitorId, favorite.getMonitorId());
|
||||
assertEquals(metricsName, favorite.getMetricsName());
|
||||
assertEquals(createTime, favorite.getCreateTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidation_ValidEntity() {
|
||||
MetricsFavorite favorite = MetricsFavorite.builder()
|
||||
.creator("testUser")
|
||||
.monitorId(1L)
|
||||
.metricsName("cpu")
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
Set<ConstraintViolation<MetricsFavorite>> violations = validator.validate(favorite);
|
||||
|
||||
assertTrue(violations.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidation_NullCreator() {
|
||||
MetricsFavorite favorite = MetricsFavorite.builder()
|
||||
.creator(null)
|
||||
.monitorId(1L)
|
||||
.metricsName("cpu")
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
Set<ConstraintViolation<MetricsFavorite>> violations = validator.validate(favorite);
|
||||
|
||||
assertFalse(violations.isEmpty());
|
||||
assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("creator")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidation_BlankCreator() {
|
||||
MetricsFavorite favorite = MetricsFavorite.builder()
|
||||
.creator(" ")
|
||||
.monitorId(1L)
|
||||
.metricsName("cpu")
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
Set<ConstraintViolation<MetricsFavorite>> violations = validator.validate(favorite);
|
||||
|
||||
assertFalse(violations.isEmpty());
|
||||
assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("creator")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidation_CreatorTooLong() {
|
||||
String longCreator = "a".repeat(256);
|
||||
MetricsFavorite favorite = MetricsFavorite.builder()
|
||||
.creator(longCreator)
|
||||
.monitorId(1L)
|
||||
.metricsName("cpu")
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
Set<ConstraintViolation<MetricsFavorite>> violations = validator.validate(favorite);
|
||||
|
||||
assertFalse(violations.isEmpty());
|
||||
assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("creator")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidation_NullMonitorId() {
|
||||
MetricsFavorite favorite = MetricsFavorite.builder()
|
||||
.creator("testUser")
|
||||
.monitorId(null)
|
||||
.metricsName("cpu")
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
Set<ConstraintViolation<MetricsFavorite>> violations = validator.validate(favorite);
|
||||
|
||||
assertFalse(violations.isEmpty());
|
||||
assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("monitorId")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidation_NullMetricsName() {
|
||||
MetricsFavorite favorite = MetricsFavorite.builder()
|
||||
.creator("testUser")
|
||||
.monitorId(1L)
|
||||
.metricsName(null)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
Set<ConstraintViolation<MetricsFavorite>> violations = validator.validate(favorite);
|
||||
|
||||
assertFalse(violations.isEmpty());
|
||||
assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("metricsName")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidation_BlankMetricsName() {
|
||||
MetricsFavorite favorite = MetricsFavorite.builder()
|
||||
.creator("testUser")
|
||||
.monitorId(1L)
|
||||
.metricsName(" ")
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
Set<ConstraintViolation<MetricsFavorite>> violations = validator.validate(favorite);
|
||||
|
||||
assertFalse(violations.isEmpty());
|
||||
assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("metricsName")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidation_MetricsNameTooLong() {
|
||||
String longMetricsName = "a".repeat(256);
|
||||
MetricsFavorite favorite = MetricsFavorite.builder()
|
||||
.creator("testUser")
|
||||
.monitorId(1L)
|
||||
.metricsName(longMetricsName)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
Set<ConstraintViolation<MetricsFavorite>> violations = validator.validate(favorite);
|
||||
|
||||
assertFalse(violations.isEmpty());
|
||||
assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("metricsName")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEqualsAndHashCode() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
MetricsFavorite favorite1 = MetricsFavorite.builder()
|
||||
.id(1L)
|
||||
.creator("testUser")
|
||||
.monitorId(1L)
|
||||
.metricsName("cpu")
|
||||
.createTime(now)
|
||||
.build();
|
||||
|
||||
MetricsFavorite favorite2 = MetricsFavorite.builder()
|
||||
.id(1L)
|
||||
.creator("testUser")
|
||||
.monitorId(1L)
|
||||
.metricsName("cpu")
|
||||
.createTime(now)
|
||||
.build();
|
||||
|
||||
MetricsFavorite favorite3 = MetricsFavorite.builder()
|
||||
.id(2L)
|
||||
.creator("testUser")
|
||||
.monitorId(1L)
|
||||
.metricsName("cpu")
|
||||
.createTime(now)
|
||||
.build();
|
||||
|
||||
assertEquals(favorite1, favorite2);
|
||||
assertEquals(favorite1.hashCode(), favorite2.hashCode());
|
||||
assertNotEquals(favorite1, favorite3);
|
||||
assertNotEquals(favorite1.hashCode(), favorite3.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToString() {
|
||||
MetricsFavorite favorite = MetricsFavorite.builder()
|
||||
.id(1L)
|
||||
.creator("testUser")
|
||||
.monitorId(1L)
|
||||
.metricsName("cpu")
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
String toString = favorite.toString();
|
||||
|
||||
assertNotNull(toString);
|
||||
assertTrue(toString.contains("MetricsFavorite"));
|
||||
assertTrue(toString.contains("testUser"));
|
||||
assertTrue(toString.contains("cpu"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreatorMaxLength() {
|
||||
String maxLengthCreator = "a".repeat(255);
|
||||
MetricsFavorite favorite = MetricsFavorite.builder()
|
||||
.creator(maxLengthCreator)
|
||||
.monitorId(1L)
|
||||
.metricsName("cpu")
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
Set<ConstraintViolation<MetricsFavorite>> violations = validator.validate(favorite);
|
||||
|
||||
assertTrue(violations.isEmpty());
|
||||
assertEquals(255, favorite.getCreator().length());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMetricsNameMaxLength() {
|
||||
String maxLengthMetricsName = "a".repeat(255);
|
||||
MetricsFavorite favorite = MetricsFavorite.builder()
|
||||
.creator("testUser")
|
||||
.monitorId(1L)
|
||||
.metricsName(maxLengthMetricsName)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
Set<ConstraintViolation<MetricsFavorite>> violations = validator.validate(favorite);
|
||||
|
||||
assertTrue(violations.isEmpty());
|
||||
assertEquals(255, favorite.getMetricsName().length());
|
||||
}
|
||||
}
|
||||
+14
-4
@@ -26,10 +26,12 @@ import org.apache.hertzbeat.common.entity.dto.ManagerMessage;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
@@ -57,16 +59,24 @@ public class ManagerSseManager {
|
||||
.name(eventName)
|
||||
.data(data));
|
||||
} catch (IOException | IllegalStateException e) {
|
||||
emitter.complete();
|
||||
removeEmitter(clientId);
|
||||
tryCompleteAndClean(clientId, emitter);
|
||||
} catch (Exception exception) {
|
||||
log.error("Failed to broadcast manager message data to client: {}", exception.getMessage());
|
||||
emitter.complete();
|
||||
removeEmitter(clientId);
|
||||
tryCompleteAndClean(clientId, emitter);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void tryCompleteAndClean(Long clientId, SseEmitter emitter) {
|
||||
try {
|
||||
Optional.ofNullable(emitter).ifPresent(ResponseBodyEmitter::complete);
|
||||
} catch (Throwable e) {
|
||||
log.debug("Failed to complete emitter for client {}: {}", clientId, e.getMessage());
|
||||
}
|
||||
// execute clear
|
||||
removeEmitter(clientId);
|
||||
}
|
||||
|
||||
public void broadcastImportTaskInProgress(String taskName, Integer progress){
|
||||
ManagerMessage managerMessage = ImportTaskMessage.createInProgressMessage(taskName, progress);
|
||||
broadcast(ManagerEventTypeEnum.IMPORT_TASK_EVENT.getValue(), JsonUtil.toJson(managerMessage));
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.manager.controller;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import com.usthe.sureness.util.SurenessContextHolder;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.entity.dto.Message;
|
||||
import org.apache.hertzbeat.manager.service.MetricsFavoriteService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.apache.hertzbeat.common.constants.CommonConstants.LOGIN_FAILED_CODE;
|
||||
|
||||
/**
|
||||
* Metrics Favorite Controller
|
||||
*/
|
||||
@Tag(name = "Metrics Favorite API")
|
||||
@RestController
|
||||
@RequestMapping(path = "/api/metrics/favorite")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class MetricsFavoriteController {
|
||||
|
||||
private final MetricsFavoriteService metricsFavoriteService;
|
||||
|
||||
@PostMapping("/{monitorId}/{metricsName}")
|
||||
@Operation(summary = "Add metrics to favorites", description = "Add specific metrics to user's favorites")
|
||||
public ResponseEntity<Message<Void>> addMetricsFavorite(
|
||||
@Parameter(description = "Monitor ID", example = "6565463543") @PathVariable Long monitorId,
|
||||
@Parameter(description = "Metrics name", example = "cpu") @PathVariable String metricsName) {
|
||||
String user = getCurrentUser();
|
||||
if (user == null) {
|
||||
return ResponseEntity.ok(Message.fail(LOGIN_FAILED_CODE, "User not authenticated"));
|
||||
}
|
||||
metricsFavoriteService.addMetricsFavorite(user, monitorId, metricsName);
|
||||
return ResponseEntity.ok(Message.success("Metrics added to favorites successfully"));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{monitorId}/{metricsName}")
|
||||
@Operation(summary = "Remove metrics from favorites", description = "Remove specific metrics from user's favorites")
|
||||
public ResponseEntity<Message<Void>> removeMetricsFavorite(
|
||||
@Parameter(description = "Monitor ID", example = "6565463543") @PathVariable Long monitorId,
|
||||
@Parameter(description = "Metrics name", example = "cpu") @PathVariable String metricsName) {
|
||||
|
||||
String user = getCurrentUser();
|
||||
if (user == null) {
|
||||
return ResponseEntity.ok(Message.fail(LOGIN_FAILED_CODE, "User not authenticated"));
|
||||
}
|
||||
metricsFavoriteService.removeMetricsFavorite(user, monitorId, metricsName);
|
||||
return ResponseEntity.ok(Message.success("Metrics removed from favorites successfully"));
|
||||
}
|
||||
|
||||
@GetMapping("/{monitorId}")
|
||||
@Operation(summary = "Get user's all favorited metrics", description = "Get all favorited metrics for current user")
|
||||
public ResponseEntity<Message<Set<String>>> getUserFavoritedMetrics(@Parameter(description = "Monitor ID", example = "6565463543") @PathVariable Long monitorId) {
|
||||
String user = getCurrentUser();
|
||||
if (user == null) {
|
||||
return ResponseEntity.ok(Message.fail(LOGIN_FAILED_CODE, "User not authenticated"));
|
||||
}
|
||||
Set<String> favoritedMetrics = metricsFavoriteService.getUserFavoritedMetrics(user, monitorId);
|
||||
return ResponseEntity.ok(Message.success(favoritedMetrics));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user ID for favorite status
|
||||
*
|
||||
* @return user id
|
||||
*/
|
||||
private String getCurrentUser() {
|
||||
try {
|
||||
SubjectSum subjectSum = SurenessContextHolder.getBindSubject();
|
||||
return String.valueOf(subjectSum.getPrincipal());
|
||||
} catch (Exception e) {
|
||||
log.error("No user found, favorites will be disabled");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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.manager.dao;
|
||||
|
||||
import org.apache.hertzbeat.common.entity.manager.MetricsFavorite;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* MetricsFavorite dao
|
||||
*/
|
||||
public interface MetricsFavoriteDao extends JpaRepository<MetricsFavorite, Long> {
|
||||
|
||||
/**
|
||||
* Find metrics favorite by creator and monitor id and metrics name
|
||||
*
|
||||
* @param creator user id
|
||||
* @param monitorId monitor id
|
||||
* @param metricsName metrics name
|
||||
* @return optional metrics favorite
|
||||
*/
|
||||
Optional<MetricsFavorite> findByCreatorAndMonitorIdAndMetricsName(String creator, Long monitorId, String metricsName);
|
||||
|
||||
/**
|
||||
* Find all metrics favorites by user id and monitor id
|
||||
*
|
||||
* @param creator user id
|
||||
* @param monitorId monitor id
|
||||
* @return list of metrics favorites
|
||||
*/
|
||||
List<MetricsFavorite> findByCreatorAndMonitorId(String creator, Long monitorId);
|
||||
|
||||
/**
|
||||
* Delete metrics favorite by user id and monitor id and metrics name
|
||||
*
|
||||
* @param creator user id
|
||||
* @param monitorId monitor id
|
||||
* @param metricsName metrics name
|
||||
*/
|
||||
@Modifying
|
||||
@Query("DELETE FROM MetricsFavorite mf WHERE mf.creator = :creator AND mf.monitorId = :monitorId AND mf.metricsName = :metricsName")
|
||||
void deleteByUserIdAndMonitorIdAndMetricsName(@Param("creator") String creator,
|
||||
@Param("monitorId") Long monitorId,
|
||||
@Param("metricsName") String metricsName);
|
||||
|
||||
/**
|
||||
* Delete metrics favorites by monitor ids
|
||||
*
|
||||
* @param monitorIds monitor ids
|
||||
*/
|
||||
@Modifying
|
||||
@Query("DELETE FROM MetricsFavorite mf WHERE mf.monitorId IN :monitorIds")
|
||||
void deleteFavoritesByMonitorIdIn(@Param("monitorIds") Set<Long> monitorIds);
|
||||
}
|
||||
+19
-4
@@ -15,12 +15,27 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.manager.pojo.dto;
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.service;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Service interface for agent operations.
|
||||
* Metrics Information with favorite status
|
||||
*/
|
||||
public interface AgentService {
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Schema(description = "Metrics information with favorite status")
|
||||
public class MetricsInfo {
|
||||
|
||||
}
|
||||
@Schema(description = "Metrics name", example = "cpu")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "Whether the metrics is favorited by current user")
|
||||
private Boolean favorited;
|
||||
}
|
||||
+1
-1
@@ -47,7 +47,7 @@ public class MonitorDto {
|
||||
private List<Param> params;
|
||||
|
||||
@Schema(description = "Monitor Metrics", accessMode = READ_ONLY)
|
||||
private List<String> metrics;
|
||||
private List<MetricsInfo> metrics;
|
||||
|
||||
@Schema(description = "pinned collector, default null if system dispatch", accessMode = READ_WRITE)
|
||||
private String collector;
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.manager.service;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Metrics Favorite Service
|
||||
*/
|
||||
public interface MetricsFavoriteService {
|
||||
|
||||
/**
|
||||
* Add metrics to favorites
|
||||
*
|
||||
* @param creator user id
|
||||
* @param monitorId monitor id
|
||||
* @param metricsName metrics name
|
||||
*/
|
||||
void addMetricsFavorite(String creator, Long monitorId, String metricsName);
|
||||
|
||||
/**
|
||||
* Remove metrics from favorites
|
||||
*
|
||||
* @param userId user id
|
||||
* @param monitorId monitor id
|
||||
* @param metricsName metrics name
|
||||
*/
|
||||
void removeMetricsFavorite(String userId, Long monitorId, String metricsName);
|
||||
|
||||
/**
|
||||
* Get user's favorited metrics names for a specific monitor
|
||||
*
|
||||
* @param userId user id
|
||||
* @param monitorId monitor id
|
||||
* @return set of favorited metrics names
|
||||
*/
|
||||
Set<String> getUserFavoritedMetrics(String userId, Long monitorId);
|
||||
|
||||
/**
|
||||
* Remove metrics from monitor ids
|
||||
*
|
||||
* @param monitorIds monitor ids
|
||||
*/
|
||||
void deleteFavoritesByMonitorIdIn(Set<Long> monitorIds);
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.manager.service.impl;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.hertzbeat.common.entity.manager.MetricsFavorite;
|
||||
import org.apache.hertzbeat.manager.dao.MetricsFavoriteDao;
|
||||
import org.apache.hertzbeat.manager.service.MetricsFavoriteService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Metrics Favorite Service Implementation
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public class MetricsFavoriteServiceImpl implements MetricsFavoriteService {
|
||||
|
||||
private final MetricsFavoriteDao metricsFavoriteDao;
|
||||
|
||||
@Override
|
||||
public void addMetricsFavorite(String creator, Long monitorId, String metricsName) {
|
||||
Optional<MetricsFavorite> existing = metricsFavoriteDao
|
||||
.findByCreatorAndMonitorIdAndMetricsName(creator, monitorId, metricsName);
|
||||
if (existing.isPresent()) {
|
||||
throw new RuntimeException("Metrics favorite already exists: " + metricsName);
|
||||
}
|
||||
MetricsFavorite favorite = MetricsFavorite.builder()
|
||||
.creator(creator)
|
||||
.monitorId(monitorId)
|
||||
.metricsName(metricsName)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
metricsFavoriteDao.save(favorite);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeMetricsFavorite(String userId, Long monitorId, String metricsName) {
|
||||
metricsFavoriteDao.deleteByUserIdAndMonitorIdAndMetricsName(userId, monitorId, metricsName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Set<String> getUserFavoritedMetrics(String userId, Long monitorId) {
|
||||
if (null == userId || null == monitorId) {
|
||||
return Set.of();
|
||||
}
|
||||
List<MetricsFavorite> favorites = metricsFavoriteDao.findByCreatorAndMonitorId(userId, monitorId);
|
||||
if (null == favorites || favorites.isEmpty()) {
|
||||
return Set.of();
|
||||
}
|
||||
return favorites.stream()
|
||||
.map(MetricsFavorite::getMetricsName)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteFavoritesByMonitorIdIn(Set<Long> monitorIds) {
|
||||
if (null == monitorIds || monitorIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
metricsFavoriteDao.deleteFavoritesByMonitorIdIn(monitorIds);
|
||||
}
|
||||
|
||||
}
|
||||
+29
-9
@@ -19,6 +19,9 @@ package org.apache.hertzbeat.manager.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.primitives.Longs;
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import com.usthe.sureness.util.SurenessContextHolder;
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -44,7 +47,6 @@ import org.apache.hertzbeat.common.entity.manager.ParamDefine;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.hertzbeat.common.support.event.MonitorDeletedEvent;
|
||||
import org.apache.hertzbeat.common.util.AesUtil;
|
||||
import org.apache.hertzbeat.common.util.CommonUtil;
|
||||
import org.apache.hertzbeat.common.util.FileUtil;
|
||||
import org.apache.hertzbeat.common.util.IntervalExpressionUtil;
|
||||
import org.apache.hertzbeat.common.util.IpDomainUtil;
|
||||
@@ -59,11 +61,13 @@ import org.apache.hertzbeat.manager.dao.MonitorBindDao;
|
||||
import org.apache.hertzbeat.manager.dao.MonitorDao;
|
||||
import org.apache.hertzbeat.manager.dao.ParamDao;
|
||||
import org.apache.hertzbeat.manager.pojo.dto.AppCount;
|
||||
import org.apache.hertzbeat.manager.pojo.dto.MetricsInfo;
|
||||
import org.apache.hertzbeat.manager.pojo.dto.MonitorDto;
|
||||
import org.apache.hertzbeat.manager.scheduler.CollectJobScheduling;
|
||||
import org.apache.hertzbeat.manager.service.AppService;
|
||||
import org.apache.hertzbeat.manager.service.ImExportService;
|
||||
import org.apache.hertzbeat.manager.service.LabelService;
|
||||
import org.apache.hertzbeat.manager.service.MetricsFavoriteService;
|
||||
import org.apache.hertzbeat.manager.service.MonitorService;
|
||||
import org.apache.hertzbeat.manager.support.exception.MonitorDatabaseException;
|
||||
import org.apache.hertzbeat.manager.support.exception.MonitorDetectException;
|
||||
@@ -138,6 +142,8 @@ public class MonitorServiceImpl implements MonitorService {
|
||||
private LabelDao labelDao;
|
||||
@Autowired
|
||||
private LabelService labelService;
|
||||
@Autowired
|
||||
private MetricsFavoriteService metricsFavoriteService;
|
||||
|
||||
public MonitorServiceImpl(List<ImExportService> imExportServiceList) {
|
||||
imExportServiceList.forEach(it -> imExportServiceMap.put(it.type(), it));
|
||||
@@ -575,6 +581,7 @@ public class MonitorServiceImpl implements MonitorService {
|
||||
Set<Long> monitorIds = monitors.stream().map(Monitor::getId).collect(Collectors.toSet());
|
||||
alertDefineBindDao.deleteAlertDefineMonitorBindsByMonitorIdIn(monitorIds);
|
||||
monitorBindDao.deleteMonitorBindByBizIdIn(monitorIds);
|
||||
metricsFavoriteService.deleteFavoritesByMonitorIdIn(monitorIds);
|
||||
for (Monitor monitor : monitors) {
|
||||
monitorBindDao.deleteByMonitorId(monitor.getId());
|
||||
collectorMonitorBindDao.deleteCollectorMonitorBindsByMonitorId(monitor.getId());
|
||||
@@ -589,24 +596,37 @@ public class MonitorServiceImpl implements MonitorService {
|
||||
public MonitorDto getMonitorDto(long id) throws RuntimeException {
|
||||
Optional<Monitor> monitorOptional = monitorDao.findById(id);
|
||||
if (monitorOptional.isPresent()) {
|
||||
// Get current user ID for favorite status
|
||||
String currentUserId = null;
|
||||
try {
|
||||
SubjectSum subjectSum = SurenessContextHolder.getBindSubject();
|
||||
currentUserId = String.valueOf(subjectSum.getPrincipal());
|
||||
} catch (Exception e) {
|
||||
log.debug("No user context found, favorites will be disabled");
|
||||
}
|
||||
Set<String> favoritedMetrics = metricsFavoriteService.getUserFavoritedMetrics(currentUserId, id);
|
||||
|
||||
Monitor monitor = monitorOptional.get();
|
||||
MonitorDto monitorDto = new MonitorDto();
|
||||
List<Param> params = paramDao.findParamsByMonitorId(id);
|
||||
monitorDto.setParams(params);
|
||||
List<MetricsInfo> metricsInfos;
|
||||
if (DispatchConstants.PROTOCOL_PROMETHEUS.equalsIgnoreCase(monitor.getApp()) || monitor.getType() == CommonConstants.MONITOR_TYPE_PUSH_AUTO_CREATE) {
|
||||
List<CollectRep.MetricsData> metricsDataList = warehouseService.queryMonitorMetricsData(id);
|
||||
List<String> metrics = metricsDataList.stream().map(CollectRep.MetricsData::getMetrics).collect(Collectors.toList());
|
||||
monitorDto.setMetrics(metrics);
|
||||
metricsInfos = metricsDataList.stream()
|
||||
.map(t -> MetricsInfo.builder().name(t.getMetrics()).favorited(favoritedMetrics.contains(t.getMetrics())).build())
|
||||
.collect(Collectors.toList());
|
||||
monitorDto.setGrafanaDashboard(dashboardService.getDashboardByMonitorId(id));
|
||||
} else {
|
||||
boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape()) || !StringUtils.hasText(monitor.getScrape());
|
||||
String type = isStatic ? monitor.getApp() : monitor.getScrape();
|
||||
Job job = appService.getAppDefine(type);
|
||||
List<String> metrics = job.getMetrics().stream()
|
||||
metricsInfos = job.getMetrics().stream()
|
||||
.filter(Metrics::isVisible)
|
||||
.map(Metrics::getName).collect(Collectors.toList());
|
||||
monitorDto.setMetrics(metrics);
|
||||
.map(t -> MetricsInfo.builder().name(t.getName()).favorited(favoritedMetrics.contains(t.getName())).build())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
monitorDto.setMetrics(metricsInfos);
|
||||
monitorDto.setMonitor(monitor);
|
||||
Optional<CollectorMonitorBind> bindOptional = collectorMonitorBindDao.findCollectorMonitorBindByMonitorId(monitor.getId());
|
||||
bindOptional.ifPresent(bind -> monitorDto.setCollector(bind.getCollector()));
|
||||
@@ -642,9 +662,9 @@ public class MonitorServiceImpl implements MonitorService {
|
||||
if (StringUtils.hasText(search)) {
|
||||
Predicate predicateHost = criteriaBuilder.like(root.get("host"), "%" + search + "%");
|
||||
Predicate predicateName = criteriaBuilder.like(criteriaBuilder.lower(root.get("name")), "%" + search.toLowerCase() + "%");
|
||||
if (CommonUtil.isNumeric(search)){
|
||||
Predicate predicateId = criteriaBuilder.equal(root.get("id"), Long.parseLong(search));
|
||||
orList.add(predicateId);
|
||||
Long id = Longs.tryParse(search);
|
||||
if (id != null) {
|
||||
orList.add(criteriaBuilder.equal(root.get("id"), id));
|
||||
}
|
||||
orList.add(predicateHost);
|
||||
orList.add(predicateName);
|
||||
|
||||
+1
-1
@@ -251,6 +251,6 @@ public class GlobalExceptionHandler {
|
||||
}
|
||||
log.error("[monitor]-[unknown error happen]-{}", errorMessage, exception);
|
||||
Message<Void> message = Message.fail(MONITOR_CONFLICT_CODE, errorMessage);
|
||||
return ResponseEntity.status(HttpStatus.CONFLICT).body(message);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,6 +208,11 @@ warehouse:
|
||||
database: public
|
||||
username: greptime
|
||||
password: greptime
|
||||
questdb:
|
||||
enabled: false
|
||||
url: localhost:9000
|
||||
username: admin
|
||||
password: quest
|
||||
iot-db:
|
||||
enabled: false
|
||||
host: 127.0.0.1
|
||||
|
||||
@@ -0,0 +1,438 @@
|
||||
# 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.
|
||||
|
||||
# The monitoring type category: service-application service monitoring, db-database monitoring, custom-custom monitoring.
|
||||
category: mid
|
||||
# The monitoring application type name (consistent with the file name)
|
||||
app: apollo
|
||||
# The monitoring i18n name
|
||||
name:
|
||||
zh-CN: Apollo配置中心
|
||||
en-US: Apollo Configuration Center
|
||||
# The description and help of this monitoring type
|
||||
help:
|
||||
zh-CN: HertzBeat 对 Apollo 配置中心的通用指标进行测量监控。<br>您可以点击 “<i>新建 Apollo 配置中心</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。<br>⚠️注意:adminservice、configservice、portal目前需要分别配置,只需要更改地址+端口即可。
|
||||
en-US: HertzBeat monitors common metrics for Apollo Configuration Centers.<br>You can click "<i>Create New Apollo Configuration Center</i>" to configure it, or select "<i>More Actions</i>" to import an existing configuration. <br>⚠️Note:adminservice, configservice, and portal currently require separate configuration. Simply modify the address and port.
|
||||
helpLink:
|
||||
zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/apollo
|
||||
en-US: https://hertzbeat.apache.org/docs/help/apollo
|
||||
# Input params define for monitoring(render web ui by the definition)
|
||||
params:
|
||||
# field-param field key
|
||||
- field: host
|
||||
# name-param field display i18n name
|
||||
name:
|
||||
zh-CN: 目标Host
|
||||
en-US: Target Host
|
||||
ja-JP: 目標ホスト
|
||||
# type-param field type(most mapping the html input type)
|
||||
type: host
|
||||
# required-true or false
|
||||
required: true
|
||||
# field-param field key
|
||||
- field: port
|
||||
# name-param field display i18n name
|
||||
name:
|
||||
zh-CN: 端口
|
||||
en-US: Port
|
||||
ja-JP: ポート
|
||||
# type-param field type(most mapping the html input type)
|
||||
type: number
|
||||
# when type is number, range is required
|
||||
range: '[0,65535]'
|
||||
required: true
|
||||
defaultValue: 8080
|
||||
# field-param field key
|
||||
- field: timeout
|
||||
# name-param field display i18n name
|
||||
name:
|
||||
zh-CN: 查询超时时间
|
||||
en-US: Query Timeout
|
||||
ja-JP: クエリタイムアウト
|
||||
type: number
|
||||
required: false
|
||||
# hide param-true or false
|
||||
hide: true
|
||||
# default value
|
||||
defaultValue: 6000
|
||||
|
||||
# collect metrics config list
|
||||
metrics:
|
||||
# Basic information indicators
|
||||
- name: application_ready_time_seconds
|
||||
i18n:
|
||||
zh-CN: 应用启动耗时
|
||||
en-US: Application ready time
|
||||
# metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel
|
||||
# priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue
|
||||
priority: 0
|
||||
# collect metrics content
|
||||
fields:
|
||||
# field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field
|
||||
- field: main_application_class
|
||||
type: 1
|
||||
label: true
|
||||
i18n:
|
||||
zh-CN: 主应用类
|
||||
en-US: Main class
|
||||
- field: metric_value
|
||||
type: 0
|
||||
unit: 's'
|
||||
i18n:
|
||||
zh-CN: 耗时
|
||||
en-US: Time Consuming
|
||||
# the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk
|
||||
protocol: http
|
||||
# the config content when protocol is http
|
||||
http:
|
||||
# http host: ipv4 ipv6 domain
|
||||
host: ^_^host^_^
|
||||
# http port
|
||||
port: ^_^port^_^
|
||||
# http url
|
||||
url: /prometheus
|
||||
# http method: GET POST PUT DELETE PATCH
|
||||
method: GET
|
||||
# http response data parse type: default-hertzbeat rule, jsonpath-jsonpath script, website-for website monitoring, prometheus-prometheus exporter rule
|
||||
parseType: prometheus
|
||||
|
||||
- name: process_uptime_seconds
|
||||
i18n:
|
||||
zh-CN: 进程已运行时间
|
||||
en-US: Process uptime
|
||||
priority: 1
|
||||
fields:
|
||||
- field: metric_value
|
||||
type: 0
|
||||
unit: 's'
|
||||
i18n:
|
||||
zh-CN: 运行时间
|
||||
en-US: uptime
|
||||
protocol: http
|
||||
http:
|
||||
host: ^_^host^_^
|
||||
port: ^_^port^_^
|
||||
url: /prometheus
|
||||
method: GET
|
||||
parseType: prometheus
|
||||
|
||||
- name: process_cpu_usage
|
||||
i18n:
|
||||
zh-CN: 进程 CPU 使用率
|
||||
en-US: Process CPU usage
|
||||
priority: 2
|
||||
fields:
|
||||
- field: metric_value
|
||||
type: 0
|
||||
unit: '%'
|
||||
i18n:
|
||||
zh-CN: 使用率
|
||||
en-US: Usage
|
||||
aliasFields:
|
||||
- metric_value
|
||||
calculates:
|
||||
- metric_value=metric_value*100
|
||||
protocol: http
|
||||
http:
|
||||
host: ^_^host^_^
|
||||
port: ^_^port^_^
|
||||
url: /prometheus
|
||||
method: GET
|
||||
parseType: prometheus
|
||||
|
||||
- name: system_cpu_usage
|
||||
i18n:
|
||||
zh-CN: 系统 CPU 使用率
|
||||
en-US: System CPU usage
|
||||
priority: 3
|
||||
fields:
|
||||
- field: metric_value
|
||||
type: 0
|
||||
unit: '%'
|
||||
i18n:
|
||||
zh-CN: 使用率
|
||||
en-US: Usage
|
||||
aliasFields:
|
||||
- metric_value
|
||||
calculates:
|
||||
- metric_value=metric_value*100
|
||||
protocol: http
|
||||
http:
|
||||
host: ^_^host^_^
|
||||
port: ^_^port^_^
|
||||
url: /prometheus
|
||||
method: GET
|
||||
parseType: prometheus
|
||||
|
||||
- name: system_load_average_1m
|
||||
i18n:
|
||||
zh-CN: 系统最近一分钟平均负载
|
||||
en-US: System load average (1m)
|
||||
priority: 4
|
||||
fields:
|
||||
- field: metric_value
|
||||
type: 0
|
||||
i18n:
|
||||
zh-CN: 负载
|
||||
en-US: load
|
||||
protocol: http
|
||||
http:
|
||||
host: ^_^host^_^
|
||||
port: ^_^port^_^
|
||||
url: /prometheus
|
||||
method: GET
|
||||
parseType: prometheus
|
||||
|
||||
- name: jvm_memory_committed_bytes
|
||||
i18n:
|
||||
zh-CN: JVM 已提交内存数
|
||||
en-US: JVM memory committed bytes
|
||||
priority: 5
|
||||
fields:
|
||||
- field: area
|
||||
label: true
|
||||
i18n:
|
||||
zh-CN: 区域
|
||||
en-US: Area
|
||||
type: 1
|
||||
- field: id
|
||||
label: true
|
||||
i18n:
|
||||
zh-CN: 内存块Id
|
||||
en-US: Memory Block Id
|
||||
type: 1
|
||||
- field: metric_value
|
||||
i18n:
|
||||
zh-CN: JVM请求内存大小(当前)
|
||||
en-US: JVM requests memory size (current)
|
||||
type: 0
|
||||
unit: MB
|
||||
units:
|
||||
- value=B->MB
|
||||
protocol: http
|
||||
http:
|
||||
host: ^_^host^_^
|
||||
port: ^_^port^_^
|
||||
url: /prometheus
|
||||
method: GET
|
||||
parseType: prometheus
|
||||
|
||||
- name: jvm_memory_used_bytes
|
||||
i18n:
|
||||
zh-CN: JVM 已使用内存数
|
||||
en-US: JVM memory used bytes
|
||||
priority: 6
|
||||
fields:
|
||||
- field: area
|
||||
label: true
|
||||
i18n:
|
||||
zh-CN: 区域
|
||||
en-US: Area
|
||||
type: 1
|
||||
- field: id
|
||||
label: true
|
||||
i18n:
|
||||
zh-CN: 内存块Id
|
||||
en-US: Memory Block Id
|
||||
type: 1
|
||||
- field: metric_value
|
||||
i18n:
|
||||
zh-CN: JVM已使用内存大小
|
||||
en-US: JVM used memory size
|
||||
type: 0
|
||||
unit: MB
|
||||
units:
|
||||
- value=B->MB
|
||||
protocol: http
|
||||
http:
|
||||
host: ^_^host^_^
|
||||
port: ^_^port^_^
|
||||
url: /prometheus
|
||||
method: GET
|
||||
parseType: prometheus
|
||||
|
||||
- name: jvm_memory_max_bytes
|
||||
i18n:
|
||||
zh-CN: JVM 最大内存数
|
||||
en-US: JVM memory max bytes
|
||||
priority: 7
|
||||
fields:
|
||||
- field: area
|
||||
label: true
|
||||
i18n:
|
||||
zh-CN: 区域
|
||||
en-US: Area
|
||||
type: 1
|
||||
- field: id
|
||||
label: true
|
||||
i18n:
|
||||
zh-CN: 内存块Id
|
||||
en-US: Memory Block Id
|
||||
type: 1
|
||||
- field: metric_value
|
||||
i18n:
|
||||
zh-CN: JVM 最大内存大小
|
||||
en-US: JVM used memory size
|
||||
type: 0
|
||||
unit: MB
|
||||
units:
|
||||
- value=B->MB
|
||||
protocol: http
|
||||
http:
|
||||
host: ^_^host^_^
|
||||
port: ^_^port^_^
|
||||
url: /prometheus
|
||||
method: GET
|
||||
parseType: prometheus
|
||||
|
||||
- name: jvm_gc_pause_seconds_count
|
||||
i18n:
|
||||
zh-CN: JVM GC 暂停次数
|
||||
en-US: Number of JVM GC pause events
|
||||
priority: 8
|
||||
fields:
|
||||
- field: action
|
||||
label: true
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 行为
|
||||
en-US: action
|
||||
- field: cause
|
||||
label: true
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 原因
|
||||
en-US: cause
|
||||
- field: metric_value
|
||||
type: 0
|
||||
i18n:
|
||||
zh-CN: 数量
|
||||
en-US: count
|
||||
protocol: http
|
||||
http:
|
||||
host: ^_^host^_^
|
||||
port: ^_^port^_^
|
||||
url: /prometheus
|
||||
method: GET
|
||||
parseType: prometheus
|
||||
|
||||
- name: jvm_gc_pause_seconds_sum
|
||||
i18n:
|
||||
zh-CN: JVM GC 暂停耗时
|
||||
en-US: Total JVM GC pause time
|
||||
priority: 9
|
||||
fields:
|
||||
- field: action
|
||||
label: true
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 行为
|
||||
en-US: action
|
||||
- field: cause
|
||||
label: true
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 原因
|
||||
en-US: cause
|
||||
- field: metric_value
|
||||
type: 0
|
||||
i18n:
|
||||
zh-CN: 耗时
|
||||
en-US: time-consuming
|
||||
protocol: http
|
||||
http:
|
||||
host: ^_^host^_^
|
||||
port: ^_^port^_^
|
||||
url: /prometheus
|
||||
method: GET
|
||||
parseType: prometheus
|
||||
|
||||
- name: jvm_memory_usage_after_gc_percent
|
||||
i18n:
|
||||
zh-CN: GC 执行后 JVM 内存使用率百分比
|
||||
en-US: JVM heap usage after garbage collection (%)
|
||||
priority: 10
|
||||
fields:
|
||||
- field: area
|
||||
label: true
|
||||
i18n:
|
||||
zh-CN: 区域
|
||||
en-US: Area
|
||||
type: 1
|
||||
- field: pool
|
||||
label: true
|
||||
i18n:
|
||||
zh-CN: 内存池
|
||||
en-US: Heap pool
|
||||
type: 1
|
||||
- field: metric_value
|
||||
type: 0
|
||||
unit: '%'
|
||||
i18n:
|
||||
zh-CN: 使用率
|
||||
en-US: Usage
|
||||
aliasFields:
|
||||
- area
|
||||
- pool
|
||||
- metric_value
|
||||
calculates:
|
||||
- area
|
||||
- pool
|
||||
- metric_value=metric_value*100
|
||||
protocol: http
|
||||
http:
|
||||
host: ^_^host^_^
|
||||
port: ^_^port^_^
|
||||
url: /prometheus
|
||||
method: GET
|
||||
parseType: prometheus
|
||||
|
||||
- name: process_files_max_files
|
||||
i18n:
|
||||
zh-CN: 允许进程打开的最大文件描述符数
|
||||
en-US: Process maximum open files
|
||||
priority: 11
|
||||
fields:
|
||||
- field: metric_value
|
||||
type: 0
|
||||
i18n:
|
||||
zh-CN: 数量
|
||||
en-US: quantity
|
||||
protocol: http
|
||||
http:
|
||||
host: ^_^host^_^
|
||||
port: ^_^port^_^
|
||||
url: /prometheus
|
||||
method: GET
|
||||
parseType: prometheus
|
||||
|
||||
- name: process_files_open_files
|
||||
i18n:
|
||||
zh-CN: 进程打开的文件描述符数量
|
||||
en-US: Process open files
|
||||
priority: 12
|
||||
fields:
|
||||
- field: metric_value
|
||||
type: 0
|
||||
i18n:
|
||||
zh-CN: 数量
|
||||
en-US: quantity
|
||||
protocol: http
|
||||
http:
|
||||
host: ^_^host^_^
|
||||
port: ^_^port^_^
|
||||
url: /prometheus
|
||||
method: GET
|
||||
parseType: prometheus
|
||||
@@ -0,0 +1,797 @@
|
||||
# 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.
|
||||
|
||||
# The monitoring type category:service-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring
|
||||
category: server
|
||||
# The monitoring type eg: linux windows tomcat mysql aws...
|
||||
app: idrac
|
||||
# The monitoring i18n name
|
||||
name:
|
||||
zh-CN: Dell iDRAC
|
||||
en-US: Dell iDRAC
|
||||
ja-JP: Dell iDRAC
|
||||
# The description and help of this monitoring type
|
||||
help:
|
||||
zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP 协议</a> 对 Dell iDRAC 的通用指标(可用性,系统信息,传感器状态等)进行采集监控。<br>您可以点击 “<i>新建 Dell iDRAC</i>” 并进行配置SNMP相关参数添加,或者选择“<i>更多操作</i>”,导入已有配置。
|
||||
en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP Protocol</a> to monitoring Dell iDRAC general performance metrics. <br>You can click the "<i>New Dell iDRAC</i>" button and config snmp params to add monitor or import an existing setup through the "<i>More Actions</i>" menu.
|
||||
ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-snmp'> SNMP プロトコルを介して</a> Dell iDRACの一般的なメトリクスを監視します。<br>「<i>新規 Dell iDRAC</i>」をクリックしてSNMPなどのパラメタを設定した後、新規することができます。
|
||||
helpLink:
|
||||
zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/idrac
|
||||
en-US: https://hertzbeat.apache.org/docs/help/idrac
|
||||
# Input params define for monitoring(render web ui by the definition)
|
||||
params:
|
||||
# field-param field key
|
||||
- field: host
|
||||
# name-param field display i18n name
|
||||
name:
|
||||
zh-CN: 目标Host
|
||||
en-US: Target Host
|
||||
ja-JP: 目標ホスト
|
||||
# type-param field type(most mapping the html input type)
|
||||
type: host
|
||||
# required-true or false
|
||||
required: true
|
||||
# field-param field key
|
||||
- field: port
|
||||
# name-param field display i18n name
|
||||
name:
|
||||
zh-CN: 端口
|
||||
en-US: Port
|
||||
ja-JP: ポート
|
||||
# type-param field type(most mapping the html input type)
|
||||
type: number
|
||||
# when type is number, range is required
|
||||
range: '[0,65535]'
|
||||
# required-true or false
|
||||
required: true
|
||||
# default value
|
||||
defaultValue: 161
|
||||
# field-param field key
|
||||
- field: snmpVersion
|
||||
# name-param field display i18n name
|
||||
name:
|
||||
zh-CN: SNMP 版本
|
||||
en-US: SNMP Version
|
||||
ja-JP: SNMPバージョン
|
||||
# type-param field type(radio mapping the html radio tag)
|
||||
type: radio
|
||||
# required-true or false
|
||||
required: true
|
||||
# when type is radio checkbox, use option to show optional values {name1:value1,name2:value2}
|
||||
options:
|
||||
- label: SNMPv1
|
||||
value: 0
|
||||
- label: SNMPv2c
|
||||
value: 1
|
||||
- label: SNMPv3
|
||||
value: 3
|
||||
# field-param field key
|
||||
- field: community
|
||||
# name-param field display i18n name
|
||||
name:
|
||||
zh-CN: SNMP 团体字
|
||||
en-US: SNMP Community
|
||||
ja-JP: SNMPコミュニティ
|
||||
# type-param field type(most mapping the html input type)
|
||||
type: text
|
||||
# when type is text, use limit to limit string length
|
||||
limit: 100
|
||||
# required-true or false
|
||||
required: false
|
||||
# param field input placeholder
|
||||
placeholder: 'Snmp community for v1 v2c'
|
||||
# dependent parameter values list
|
||||
depend:
|
||||
snmpVersion:
|
||||
- 0
|
||||
- 1
|
||||
# field-param field key
|
||||
- field: username
|
||||
# name-param field display i18n name
|
||||
name:
|
||||
zh-CN: SNMP username
|
||||
en-US: SNMP username
|
||||
ja-JP: SNMPユーザー名
|
||||
# type-param field type(most mapping the html input type)
|
||||
type: text
|
||||
# when type is text, use limit to limit string length
|
||||
limit: 50
|
||||
# required-true or false
|
||||
required: false
|
||||
# param field input placeholder
|
||||
placeholder: 'Snmp username for v3'
|
||||
# dependent parameter values list
|
||||
depend:
|
||||
snmpVersion:
|
||||
- 3
|
||||
# field-param field key
|
||||
- field: contextName
|
||||
# name-param field display i18n name
|
||||
name:
|
||||
zh-CN: SNMP contextName
|
||||
en-US: SNMP contextName
|
||||
ja-JP: SNMPコンテキスト名
|
||||
# type-param field type(most mapping the html input type)
|
||||
type: text
|
||||
# when type is text, use limit to limit string length
|
||||
limit: 100
|
||||
# required-true or false
|
||||
required: false
|
||||
# param field input placeholder
|
||||
placeholder: 'Snmp contextName for v3'
|
||||
# dependent parameter values list
|
||||
depend:
|
||||
snmpVersion:
|
||||
- 3
|
||||
# field-param field key
|
||||
- field: authPassphrase
|
||||
# name-param field display i18n name
|
||||
name:
|
||||
zh-CN: SNMP authPassword
|
||||
en-US: SNMP authPassword
|
||||
ja-JP: SNMP認証パスワード
|
||||
# type-param field type(most mapping the html input type)
|
||||
type: text
|
||||
# when type is text, use limit to limit string length
|
||||
limit: 100
|
||||
# required-true or false
|
||||
required: false
|
||||
# param field input placeholder
|
||||
placeholder: 'Snmp authPassword for v3'
|
||||
# dependent parameter values list
|
||||
depend:
|
||||
snmpVersion:
|
||||
- 3
|
||||
# field-param field key
|
||||
- field: authPasswordEncryption
|
||||
# name-param field display i18n name
|
||||
name:
|
||||
zh-CN: authPassword 加密方式
|
||||
en-US: authPassword Encryption
|
||||
ja-JP: 認証暗号
|
||||
# type-param field type(radio mapping the html radio tag)
|
||||
type: radio
|
||||
# required-true or false
|
||||
required: false
|
||||
# when type is radio checkbox, use option to show optional values {name1:value1,name2:value2}
|
||||
options:
|
||||
- label: MD5
|
||||
value: 0
|
||||
- label: SHA1
|
||||
value: 1
|
||||
# dependent parameter values list
|
||||
depend:
|
||||
snmpVersion:
|
||||
- 3
|
||||
# field-param field key
|
||||
- field: privPassphrase
|
||||
# name-param field display i18n name
|
||||
name:
|
||||
zh-CN: SNMP privPassphrase
|
||||
en-US: SNMP privPassphrase
|
||||
ja-JP: SNMPパスワードフレーズ
|
||||
# type-param field type(most mapping the html input type)
|
||||
type: text
|
||||
# when type is text, use limit to limit string length
|
||||
limit: 100
|
||||
# required-true or false
|
||||
required: false
|
||||
# param field input placeholder
|
||||
placeholder: 'Snmp authPassword for v3'
|
||||
# dependent parameter values list
|
||||
depend:
|
||||
snmpVersion:
|
||||
- 3
|
||||
# field-param field key
|
||||
- field: privPasswordEncryption
|
||||
# name-param field display i18n name
|
||||
name:
|
||||
zh-CN: privPassword 加密方式
|
||||
en-US: privPassword Encryption
|
||||
ja-JP: パスワードの暗号
|
||||
# type-param field type(radio mapping the html radio tag)
|
||||
type: radio
|
||||
# required-true or false
|
||||
required: false
|
||||
# when type is radio checkbox, use option to show optional values {name1:value1,name2:value2}
|
||||
options:
|
||||
- label: DES
|
||||
value: 0
|
||||
- label: AES128
|
||||
value: 1
|
||||
# dependent parameter values list
|
||||
depend:
|
||||
snmpVersion:
|
||||
- 3
|
||||
# field-param field key
|
||||
- field: timeout
|
||||
# name-param field display i18n name
|
||||
name:
|
||||
zh-CN: 超时时间(ms)
|
||||
en-US: Timeout(ms)
|
||||
ja-JP: タイムアウト(ms)
|
||||
# type-param field type(most mapping the html input type)
|
||||
type: number
|
||||
# when type is number, range is required
|
||||
range: '[0,100000]'
|
||||
# required-true or false
|
||||
required: false
|
||||
# hide-is hide this field and put it in advanced layout
|
||||
hide: true
|
||||
# default value
|
||||
defaultValue: 6000
|
||||
# collect metrics config list
|
||||
metrics:
|
||||
# metrics - system
|
||||
- name: system
|
||||
i18n:
|
||||
zh-CN: 系统状态
|
||||
en-US: System Status
|
||||
ja-JP: システム情報
|
||||
# metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel
|
||||
# priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue
|
||||
priority: 0
|
||||
# collect metrics content
|
||||
fields:
|
||||
# field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field
|
||||
- field: global
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 全局系统状态
|
||||
en-US: Global System Status
|
||||
ja-JP: システム状態
|
||||
- field: lcd
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 系统前面板状态
|
||||
en-US: System LCD Status
|
||||
ja-JP: システムLCDの状態
|
||||
- field: storage
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 全局存储状态
|
||||
en-US: Global Storage Status
|
||||
ja-JP: ストレージ状態
|
||||
- field: power
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 系统电源状态
|
||||
en-US: System Power State
|
||||
ja-JP: システム電源状態
|
||||
- field: up_time
|
||||
type: 1
|
||||
unit: day
|
||||
i18n:
|
||||
zh-CN: 系统运行时间
|
||||
en-US: System Power Up Time
|
||||
ja-JP: アップタイム
|
||||
# (optional)metrics field alias name, it is used as an alias field to map and convert the collected data and metrics field
|
||||
aliasFields:
|
||||
- globalSystemStatusEnum
|
||||
- systemLCDStatusEnum
|
||||
- globalStorageStatusEnum
|
||||
- systemPowerStateEnum
|
||||
- systemPowerUpTime
|
||||
|
||||
# mapping and conversion expressions, use these and aliasField above to calculate metrics value
|
||||
# eg: cores=core1+core2, usage=usage, waitTime=allTime-runningTime
|
||||
calculates:
|
||||
- "global=(globalSystemStatusEnum == 1) ? 'other' : (globalSystemStatusEnum == 2) ? 'unknown' : (globalSystemStatusEnum == 3) ? 'ok' : (globalSystemStatusEnum == 4) ? 'nonCritical' : (globalSystemStatusEnum == 5) ? 'critical' : (globalSystemStatusEnum == 6) ? 'nonRecoverable' : 'unknown'"
|
||||
- "lcd=(systemLCDStatusEnum == 1) ? 'other' : (systemLCDStatusEnum == 2) ? 'unknown' : (systemLCDStatusEnum == 3) ? 'ok' : (systemLCDStatusEnum == 4) ? 'nonCritical' : (systemLCDStatusEnum == 5) ? 'critical' : (systemLCDStatusEnum == 6) ? 'nonRecoverable' : 'unknown'"
|
||||
- "storage=(globalStorageStatusEnum == 1) ? 'other' : (globalStorageStatusEnum == 2) ? 'unknown' : (globalStorageStatusEnum == 3) ? 'ok' : (globalStorageStatusEnum == 4) ? 'nonCritical' : (globalStorageStatusEnum == 5) ? 'critical' : (globalStorageStatusEnum == 6) ? 'nonRecoverable' : 'unknown'"
|
||||
- "power=(systemPowerStateEnum == 1) ? 'other' : (systemPowerStateEnum == 2) ? 'unknown' : (systemPowerStateEnum == 3) ? 'off' : (systemPowerStateEnum == 4) ? 'ok' : 'unknown'"
|
||||
- up_time=systemPowerUpTime/60/60/24
|
||||
|
||||
# the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk
|
||||
protocol: snmp
|
||||
# the config content when protocol is snmp
|
||||
snmp:
|
||||
# server host: ipv4 ipv6 domain
|
||||
host: ^_^host^_^
|
||||
# server port
|
||||
port: ^_^port^_^
|
||||
# snmp username
|
||||
username: ^_^username^_^
|
||||
# snmp authPassphrase
|
||||
authPassphrase: ^_^authPassphrase^_^
|
||||
# snmp privPassphrase
|
||||
privPassphrase: ^_^privPassphrase^_^
|
||||
# snmp privPasswordEncryption
|
||||
privPasswordEncryption: ^_^privPasswordEncryption^_^
|
||||
# snmp authPasswordEncryption
|
||||
authPasswordEncryption: ^_^authPasswordEncryption^_^
|
||||
# contextName
|
||||
contextName: ^_^contextName^_^
|
||||
# snmp connect timeout
|
||||
timeout: ^_^timeout^_^
|
||||
# snmp community
|
||||
community: ^_^community^_^
|
||||
# snmp version
|
||||
version: ^_^snmpVersion^_^
|
||||
# snmp operation: get, walk
|
||||
operation: get
|
||||
# metrics oids: metric_name - oid_value
|
||||
oids:
|
||||
globalSystemStatusEnum: 1.3.6.1.4.1.674.10892.5.2.1.0
|
||||
systemLCDStatusEnum: 1.3.6.1.4.1.674.10892.5.2.2.0
|
||||
globalStorageStatusEnum: 1.3.6.1.4.1.674.10892.5.2.3.0
|
||||
systemPowerStateEnum: 1.3.6.1.4.1.674.10892.5.2.4.0
|
||||
systemPowerUpTime: 1.3.6.1.4.1.674.10892.5.2.5.0
|
||||
|
||||
- name: power_supply
|
||||
i18n:
|
||||
zh-CN: 电源供应器
|
||||
en-US: Power Supply
|
||||
ja-JP: 電源供給ユニット
|
||||
priority: 1
|
||||
fields:
|
||||
- field: index
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 索引
|
||||
en-US: Index
|
||||
ja-JP: 番号
|
||||
- field: name
|
||||
type: 1
|
||||
label: true
|
||||
i18n:
|
||||
zh-CN: 名称
|
||||
en-US: Name
|
||||
ja-JP: 名前
|
||||
- field: wattage
|
||||
type: 1
|
||||
unit: W
|
||||
i18n:
|
||||
zh-CN: 额定功率
|
||||
en-US: Wattage
|
||||
ja-JP: ワット数
|
||||
- field: input_wattage
|
||||
type: 0
|
||||
unit: W
|
||||
i18n:
|
||||
zh-CN: 输入功率
|
||||
en-US: Input Wattage
|
||||
ja-JP: 入力ワット数
|
||||
- field: max_input_voltage
|
||||
type: 1
|
||||
unit: V
|
||||
i18n:
|
||||
zh-CN: 最大输入电压
|
||||
en-US: Max Input Voltage
|
||||
ja-JP: 最大入力電圧
|
||||
- field: type
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 类型
|
||||
en-US: Type
|
||||
ja-JP: タイプ
|
||||
- field: status
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 状态
|
||||
en-US: Status
|
||||
ja-JP: 状態
|
||||
aliasFields:
|
||||
- powerSupplyIndex
|
||||
- powerSupplyLocationName
|
||||
- powerSupplyStatusEnum
|
||||
- powerSupplyOutputWatts
|
||||
- powerSupplyTypeEnum
|
||||
- powerSupplyRatedInputWattage
|
||||
- powerSupplyMaximumInputVoltage
|
||||
calculates:
|
||||
- "index=powerSupplyIndex"
|
||||
- "name=powerSupplyLocationName"
|
||||
- "status=(powerSupplyStatusEnum == 1) ? 'other' : (powerSupplyStatusEnum == 2) ? 'unknown' : (powerSupplyStatusEnum == 3) ? 'ok' : (powerSupplyStatusEnum == 4) ? 'nonCritical' : (powerSupplyStatusEnum == 5) ? 'critical' : (powerSupplyStatusEnum == 6) ? 'nonRecoverable' : 'unknown'"
|
||||
- "location=powerSupplyLocationName"
|
||||
- "wattage=powerSupplyOutputWatts / 10"
|
||||
- "type=(powerSupplyTypeEnum == 1) ? 'other' : (powerSupplyTypeEnum == 2) ? 'unknown' : (powerSupplyTypeEnum == 3) ? 'Linear' : (powerSupplyTypeEnum == 4) ? 'Switching' : (powerSupplyTypeEnum == 5) ? 'Battery' : (powerSupplyTypeEnum == 6) ? 'UPS' : (powerSupplyTypeEnum == 7) ? 'Converter' : (powerSupplyTypeEnum == 8) ? 'Regulator' : (powerSupplyTypeEnum == 9) ? 'AC' : (powerSupplyTypeEnum == 10) ? 'DC' : (powerSupplyTypeEnum == 11) ? 'VRM' : 'unknown'"
|
||||
- "input_wattage=powerSupplyRatedInputWattage / 10"
|
||||
- "max_input_voltage=powerSupplyMaximumInputVoltage"
|
||||
protocol: snmp
|
||||
snmp:
|
||||
host: ^_^host^_^
|
||||
port: ^_^port^_^
|
||||
username: ^_^username^_^
|
||||
authPassphrase: ^_^authPassphrase^_^
|
||||
privPassphrase: ^_^privPassphrase^_^
|
||||
privPasswordEncryption: ^_^privPasswordEncryption^_^
|
||||
authPasswordEncryption: ^_^authPasswordEncryption^_^
|
||||
contextName: ^_^contextName^_^
|
||||
timeout: ^_^timeout^_^
|
||||
community: ^_^community^_^
|
||||
version: ^_^snmpVersion^_^
|
||||
operation: walk
|
||||
oids:
|
||||
powerSupplyIndex: 1.3.6.1.4.1.674.10892.5.4.600.12.1.2
|
||||
powerSupplyLocationName: 1.3.6.1.4.1.674.10892.5.4.600.12.1.8
|
||||
powerSupplyStatusEnum: 1.3.6.1.4.1.674.10892.5.4.600.12.1.5
|
||||
powerSupplyTypeEnum: 1.3.6.1.4.1.674.10892.5.4.600.12.1.7
|
||||
powerSupplyOutputWatts: 1.3.6.1.4.1.674.10892.5.4.600.12.1.6
|
||||
powerSupplyRatedInputWattage: 1.3.6.1.4.1.674.10892.5.4.600.12.1.14
|
||||
powerSupplyMaximumInputVoltage: 1.3.6.1.4.1.674.10892.5.4.600.12.1.9
|
||||
|
||||
- name: cooling
|
||||
i18n:
|
||||
zh-CN: 冷却设备
|
||||
en-US: Cooling Devices
|
||||
ja-JP: クーリング情報
|
||||
priority: 1
|
||||
fields:
|
||||
- field: index
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 索引
|
||||
en-US: Index
|
||||
ja-JP: 番号
|
||||
- field: name
|
||||
type: 1
|
||||
label: true
|
||||
i18n:
|
||||
zh-CN: 名称
|
||||
en-US: Name
|
||||
ja-JP: 名前
|
||||
- field: type
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 类型
|
||||
en-US: Type
|
||||
ja-JP: タイプ
|
||||
- field: reading
|
||||
type: 0
|
||||
unit: RPM
|
||||
i18n:
|
||||
zh-CN: 转速
|
||||
en-US: Reading
|
||||
ja-JP: 回転数
|
||||
- field: status
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 状态
|
||||
en-US: Status
|
||||
ja-JP: 状態
|
||||
aliasFields:
|
||||
- coolingDeviceIndex
|
||||
- coolingDeviceLocationName
|
||||
- coolingDeviceStatusEnum
|
||||
- coolingDeviceReading
|
||||
- coolingDeviceTypeEnum
|
||||
calculates:
|
||||
- "index=coolingDeviceIndex"
|
||||
- "name=coolingDeviceLocationName"
|
||||
- "status=(coolingDeviceStatusEnum == 1) ? 'other' : (coolingDeviceStatusEnum == 2) ? 'unknown' : (coolingDeviceStatusEnum == 3) ? 'ok' : (coolingDeviceStatusEnum == 4) ? 'nonCriticalUpper' : (coolingDeviceStatusEnum == 5) ? 'criticalUpper' : (coolingDeviceStatusEnum == 6) ? 'nonRecoverableUpper' : (coolingDeviceStatusEnum == 7) ? 'noncriticalLower' : (coolingDeviceStatusEnum == 8) ? 'criticalLower' : (coolingDeviceStatusEnum == 9) ? 'nonRecoverableLower' : (coolingDeviceStatusEnum == 10) ? 'failed' : 'unknown'"
|
||||
- "reading=coolingDeviceReading"
|
||||
- "type=(coolingDeviceTypeEnum == 1) ? 'other' : (coolingDeviceTypeEnum == 2) ? 'unknown' : (coolingDeviceTypeEnum == 3) ? 'AFan' : (coolingDeviceTypeEnum == 4) ? 'ABlower' : (coolingDeviceTypeEnum == 5) ? 'AChipFan' : (coolingDeviceTypeEnum == 6) ? 'ACabinetFan' : (coolingDeviceTypeEnum == 7) ? 'APowerSupplyFan' : (coolingDeviceTypeEnum == 8) ? 'AHeatPipe' : (coolingDeviceTypeEnum == 9) ? 'Refrigeration' : (coolingDeviceTypeEnum == 10) ? 'ActiveCooling' : (coolingDeviceTypeEnum == 11) ? 'PassiveCooling' : 'unknown'"
|
||||
protocol: snmp
|
||||
snmp:
|
||||
host: ^_^host^_^
|
||||
port: ^_^port^_^
|
||||
username: ^_^username^_^
|
||||
authPassphrase: ^_^authPassphrase^_^
|
||||
privPassphrase: ^_^privPassphrase^_^
|
||||
privPasswordEncryption: ^_^privPasswordEncryption^_^
|
||||
authPasswordEncryption: ^_^authPasswordEncryption^_^
|
||||
contextName: ^_^contextName^_^
|
||||
timeout: ^_^timeout^_^
|
||||
community: ^_^community^_^
|
||||
version: ^_^snmpVersion^_^
|
||||
operation: walk
|
||||
oids:
|
||||
coolingDeviceIndex: 1.3.6.1.4.1.674.10892.5.4.700.12.1.2
|
||||
coolingDeviceLocationName: 1.3.6.1.4.1.674.10892.5.4.700.12.1.8
|
||||
coolingDeviceStatusEnum: 1.3.6.1.4.1.674.10892.5.4.700.12.1.5
|
||||
coolingDeviceReading: 1.3.6.1.4.1.674.10892.5.4.700.12.1.6
|
||||
coolingDeviceTypeEnum: 1.3.6.1.4.1.674.10892.5.4.700.12.1.7
|
||||
|
||||
- name: temperature
|
||||
i18n:
|
||||
zh-CN: 温度传感器
|
||||
en-US: Temperature Sensors
|
||||
ja-JP: 内部温度情報
|
||||
priority: 1
|
||||
fields:
|
||||
- field: index
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 索引
|
||||
en-US: Index
|
||||
ja-JP: 番号
|
||||
- field: name
|
||||
type: 1
|
||||
label: true
|
||||
i18n:
|
||||
zh-CN: 名称
|
||||
en-US: Name
|
||||
ja-JP: 名前
|
||||
- field: reading
|
||||
type: 0
|
||||
unit: °C
|
||||
i18n:
|
||||
zh-CN: 温度
|
||||
en-US: Temperature
|
||||
ja-JP: 温度
|
||||
- field: status
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 状态
|
||||
en-US: Status
|
||||
ja-JP: 状態
|
||||
aliasFields:
|
||||
- temperatureProbeIndex
|
||||
- temperatureProbeLocationName
|
||||
- temperatureProbeStatusEnum
|
||||
- temperatureProbeReading
|
||||
calculates:
|
||||
- "index=temperatureProbeIndex"
|
||||
- "name=temperatureProbeLocationName"
|
||||
- "status=(temperatureProbeStatusEnum == 1) ? 'other' : (temperatureProbeStatusEnum == 2) ? 'unknown' : (temperatureProbeStatusEnum == 3) ? 'ok' : (temperatureProbeStatusEnum == 4) ? 'nonCriticalUpper' : (temperatureProbeStatusEnum == 5) ? 'criticalUpper' : (temperatureProbeStatusEnum == 6) ? 'nonRecoverableUpper' : (temperatureProbeStatusEnum == 7) ? 'noncriticalLower' : (temperatureProbeStatusEnum == 8) ? 'criticalLower' : (temperatureProbeStatusEnum == 9) ? 'nonRecoverableLower' : (temperatureProbeStatusEnum == 10) ? 'failed' : 'unknown'"
|
||||
- "reading=temperatureProbeReading / 10"
|
||||
protocol: snmp
|
||||
snmp:
|
||||
host: ^_^host^_^
|
||||
port: ^_^port^_^
|
||||
username: ^_^username^_^
|
||||
authPassphrase: ^_^authPassphrase^_^
|
||||
privPassphrase: ^_^privPassphrase^_^
|
||||
privPasswordEncryption: ^_^privPasswordEncryption^_^
|
||||
authPasswordEncryption: ^_^authPasswordEncryption^_^
|
||||
contextName: ^_^contextName^_^
|
||||
timeout: ^_^timeout^_^
|
||||
community: ^_^community^_^
|
||||
version: ^_^snmpVersion^_^
|
||||
operation: walk
|
||||
oids:
|
||||
temperatureProbeIndex: 1.3.6.1.4.1.674.10892.5.4.700.20.1.2
|
||||
temperatureProbeLocationName: 1.3.6.1.4.1.674.10892.5.4.700.20.1.8
|
||||
temperatureProbeStatusEnum: 1.3.6.1.4.1.674.10892.5.4.700.20.1.5
|
||||
temperatureProbeReading: 1.3.6.1.4.1.674.10892.5.4.700.20.1.6
|
||||
|
||||
- name: voltage
|
||||
i18n:
|
||||
zh-CN: 电压传感器
|
||||
en-US: Voltage Sensors
|
||||
ja-JP: 電圧情報
|
||||
priority: 1
|
||||
fields:
|
||||
- field: index
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 索引
|
||||
en-US: Index
|
||||
ja-JP: 番号
|
||||
- field: name
|
||||
type: 1
|
||||
label: true
|
||||
i18n:
|
||||
zh-CN: 名称
|
||||
en-US: Name
|
||||
ja-JP: 名前
|
||||
- field: reading
|
||||
type: 0
|
||||
unit: V
|
||||
i18n:
|
||||
zh-CN: 电压
|
||||
en-US: Voltage
|
||||
ja-JP: 電圧
|
||||
- field: type
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 类型
|
||||
en-US: Type
|
||||
ja-JP: タイプ
|
||||
- field: status
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 状态
|
||||
en-US: Status
|
||||
ja-JP: 状態
|
||||
aliasFields:
|
||||
- voltageProbeIndex
|
||||
- voltageProbeLocationName
|
||||
- voltageProbeStatusEnum
|
||||
- voltageProbeReading
|
||||
- voltageProbeTypeEnum
|
||||
calculates:
|
||||
- "index=voltageProbeIndex"
|
||||
- "name=voltageProbeLocationName"
|
||||
- "status=(voltageProbeStatusEnum == 1) ? 'other' : (voltageProbeStatusEnum == 2) ? 'unknown' : (voltageProbeStatusEnum == 3) ? 'ok' : (voltageProbeStatusEnum == 4) ? 'nonCriticalUpper' : (voltageProbeStatusEnum == 5) ? 'criticalUpper' : (voltageProbeStatusEnum == 6) ? 'nonRecoverableUpper' : (voltageProbeStatusEnum == 7) ? 'nonCriticalLower' : (voltageProbeStatusEnum == 8) ? 'criticalLower' : (voltageProbeStatusEnum == 9) ? 'nonRecoverableLower' : (voltageProbeStatusEnum == 10) ? 'failed' : 'unknown'"
|
||||
- "reading=voltageProbeReading / 1000"
|
||||
- "type=(voltageProbeTypeEnum == 1) ? 'other' : (voltageProbeTypeEnum == 2) ? 'unknown' : (voltageProbeTypeEnum == 3) ? '1.5V' : (voltageProbeTypeEnum == 4) ? '3.3V' : (voltageProbeTypeEnum == 5) ? '5V' : (voltageProbeTypeEnum == 6) ? '-5V' : (voltageProbeTypeEnum == 7) ? '12V' : (voltageProbeTypeEnum == 8) ? '-12V' : (voltageProbeTypeEnum == 9) ? 'IO' : (voltageProbeTypeEnum == 10) ? 'Core' : (voltageProbeTypeEnum == 11) ? 'FLEA' : (voltageProbeTypeEnum == 12) ? 'Battery' : (voltageProbeTypeEnum == 13) ? 'Terminator' : (voltageProbeTypeEnum == 14) ? '2.5V' : (voltageProbeTypeEnum == 15) ? 'GTL' : (voltageProbeTypeEnum == 16) ? 'Discrete' : (voltageProbeTypeEnum == 17) ? 'GenericDiscrete' : (voltageProbeTypeEnum == 18) ? 'PSVoltage' : (voltageProbeTypeEnum == 19) ? 'MemoryStatus' : 'unknown'"
|
||||
protocol: snmp
|
||||
snmp:
|
||||
host: ^_^host^_^
|
||||
port: ^_^port^_^
|
||||
username: ^_^username^_^
|
||||
authPassphrase: ^_^authPassphrase^_^
|
||||
privPassphrase: ^_^privPassphrase^_^
|
||||
privPasswordEncryption: ^_^privPasswordEncryption^_^
|
||||
authPasswordEncryption: ^_^authPasswordEncryption^_^
|
||||
contextName: ^_^contextName^_^
|
||||
timeout: ^_^timeout^_^
|
||||
community: ^_^community^_^
|
||||
version: ^_^snmpVersion^_^
|
||||
operation: walk
|
||||
oids:
|
||||
voltageProbeIndex: 1.3.6.1.4.1.674.10892.5.4.600.20.1.2
|
||||
voltageProbeLocationName: 1.3.6.1.4.1.674.10892.5.4.600.20.1.8
|
||||
voltageProbeStatusEnum: 1.3.6.1.4.1.674.10892.5.4.600.20.1.5
|
||||
voltageProbeReading: 1.3.6.1.4.1.674.10892.5.4.600.20.1.6
|
||||
voltageProbeTypeEnum: 1.3.6.1.4.1.674.10892.5.4.600.20.1.7
|
||||
|
||||
|
||||
- name: memory
|
||||
i18n:
|
||||
zh-CN: 内存设备
|
||||
en-US: Memory Devices
|
||||
ja-JP: メモリ情報
|
||||
priority: 1
|
||||
fields:
|
||||
- field: index
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 索引
|
||||
en-US: Index
|
||||
ja-JP: 番号
|
||||
- field: name
|
||||
type: 1
|
||||
label: true
|
||||
i18n:
|
||||
zh-CN: 名称
|
||||
en-US: Name
|
||||
ja-JP: 名前
|
||||
- field: type
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 类型
|
||||
en-US: Type
|
||||
ja-JP: タイプ
|
||||
- field: sizes
|
||||
type: 1
|
||||
unit: G
|
||||
i18n:
|
||||
zh-CN: 容量
|
||||
en-US: Size
|
||||
ja-JP: メモリ容量
|
||||
- field: speed
|
||||
type: 1
|
||||
unit: MHz
|
||||
i18n:
|
||||
zh-CN: 频率
|
||||
en-US: Speed
|
||||
ja-JP: メモリの最大速度
|
||||
- field: current_speed
|
||||
type: 0
|
||||
unit: MHz
|
||||
i18n:
|
||||
zh-CN: 运行频率
|
||||
en-US: Current Speed
|
||||
ja-JP: 現在のメモリ速度
|
||||
- field: status
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 状态
|
||||
en-US: Status
|
||||
ja-JP: 状態
|
||||
aliasFields:
|
||||
- memoryDeviceIndex
|
||||
- memoryDeviceLocationName
|
||||
- memoryDeviceStatusEnum
|
||||
- memoryDeviceSize
|
||||
- memoryDeviceTypeEnum
|
||||
- memoryDeviceSpeed
|
||||
- memoryDeviceCurrentOperatingSpeed
|
||||
calculates:
|
||||
- "index=memoryDeviceIndex"
|
||||
- "name=memoryDeviceLocationName"
|
||||
- "status=(memoryDeviceStatusEnum == 1) ? 'other' : (memoryDeviceStatusEnum == 2) ? 'unknown' : (memoryDeviceStatusEnum == 3) ? 'ok' : (memoryDeviceStatusEnum == 4) ? 'nonCritical' : (memoryDeviceStatusEnum == 5) ? 'critical' : (memoryDeviceStatusEnum == 6) ? 'nonRecoverable' : 'unknown'"
|
||||
- "sizes=memoryDeviceSize/1024/1024"
|
||||
- "type=(memoryDeviceTypeEnum == 1) ? 'other' : (memoryDeviceTypeEnum == 2) ? 'unknown' : (memoryDeviceTypeEnum == 3) ? 'DRAM' : (memoryDeviceTypeEnum == 4) ? 'EDRAM' : (memoryDeviceTypeEnum == 5) ? 'VRAM' : (memoryDeviceTypeEnum == 6) ? 'SRAM' : (memoryDeviceTypeEnum == 7) ? 'RAM' : (memoryDeviceTypeEnum == 8) ? 'ROM' : (memoryDeviceTypeEnum == 9) ? 'FLASH' : (memoryDeviceTypeEnum == 10) ? 'EEPROM' : (memoryDeviceTypeEnum == 11) ? 'FEPROM' : (memoryDeviceTypeEnum == 12) ? 'EPROM' : (memoryDeviceTypeEnum == 13) ? 'CDRAM' : (memoryDeviceTypeEnum == 14) ? '3DRAM' : (memoryDeviceTypeEnum == 15) ? 'SDRAM' : (memoryDeviceTypeEnum == 16) ? 'SGRAM' : (memoryDeviceTypeEnum == 17) ? 'RDRAM' : (memoryDeviceTypeEnum == 18) ? 'DDR' : (memoryDeviceTypeEnum == 19) ? 'DDR2' : (memoryDeviceTypeEnum == 20) ? 'DDR2FBDIMM' : (memoryDeviceTypeEnum == 24) ? 'DDR3' : (memoryDeviceTypeEnum == 25) ? 'FBD2' : (memoryDeviceTypeEnum == 26) ? 'DDR4' : 'unknown'"
|
||||
- "speed=memoryDeviceSpeed"
|
||||
- "current_speed=memoryDeviceCurrentOperatingSpeed"
|
||||
|
||||
protocol: snmp
|
||||
snmp:
|
||||
host: ^_^host^_^
|
||||
port: ^_^port^_^
|
||||
username: ^_^username^_^
|
||||
authPassphrase: ^_^authPassphrase^_^
|
||||
privPassphrase: ^_^privPassphrase^_^
|
||||
privPasswordEncryption: ^_^privPasswordEncryption^_^
|
||||
authPasswordEncryption: ^_^authPasswordEncryption^_^
|
||||
contextName: ^_^contextName^_^
|
||||
timeout: ^_^timeout^_^
|
||||
community: ^_^community^_^
|
||||
version: ^_^snmpVersion^_^
|
||||
operation: walk
|
||||
oids:
|
||||
memoryDeviceIndex: 1.3.6.1.4.1.674.10892.5.4.1100.50.1.2
|
||||
memoryDeviceLocationName: 1.3.6.1.4.1.674.10892.5.4.1100.50.1.8
|
||||
memoryDeviceStatusEnum: 1.3.6.1.4.1.674.10892.5.4.1100.50.1.5
|
||||
memoryDeviceSize: 1.3.6.1.4.1.674.10892.5.4.1100.50.1.14
|
||||
memoryDeviceTypeEnum: 1.3.6.1.4.1.674.10892.5.4.1100.50.1.7
|
||||
memoryDeviceSpeed: 1.3.6.1.4.1.674.10892.5.4.1100.50.1.15
|
||||
memoryDeviceCurrentOperatingSpeed: 1.3.6.1.4.1.674.10892.5.4.1100.50.1.27
|
||||
|
||||
- name: processor
|
||||
i18n:
|
||||
zh-CN: 处理器
|
||||
en-US: Processors
|
||||
ja-JP: プロセッサ情報
|
||||
priority: 1
|
||||
fields:
|
||||
- field: index
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 索引
|
||||
en-US: Index
|
||||
ja-JP: 番号
|
||||
- field: name
|
||||
type: 1
|
||||
label: true
|
||||
i18n:
|
||||
zh-CN: 名称
|
||||
en-US: Name
|
||||
ja-JP: 名前
|
||||
- field: speed
|
||||
type: 0
|
||||
unit: MHz
|
||||
i18n:
|
||||
zh-CN: 当前频率
|
||||
en-US: Current Speed
|
||||
ja-JP: プロセッサ速度
|
||||
- field: family
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 家族
|
||||
en-US: Family
|
||||
ja-JP: ファミリー
|
||||
- field: status
|
||||
type: 1
|
||||
i18n:
|
||||
zh-CN: 状态
|
||||
en-US: Status
|
||||
ja-JP: 状態
|
||||
aliasFields:
|
||||
- processorDeviceIndex
|
||||
- processorDeviceLocationName
|
||||
- processorDeviceStatusEnum
|
||||
- processorDeviceSpeed
|
||||
- processorDeviceFamilyEnum
|
||||
calculates:
|
||||
- "index=processorDeviceIndex"
|
||||
- "name=processorDeviceLocationName"
|
||||
- "status=(processorDeviceStatusEnum == 1) ? 'other' : (processorDeviceStatusEnum == 2) ? 'unknown' : (processorDeviceStatusEnum == 3) ? 'ok' : (processorDeviceStatusEnum == 4) ? 'nonCritical' : (processorDeviceStatusEnum == 5) ? 'critical' : (processorDeviceStatusEnum == 6) ? 'nonRecoverable' : 'unknown'"
|
||||
- "speed=processorDeviceSpeed"
|
||||
- "family=(processorDeviceFamilyEnum == 1) ? 'other' : (processorDeviceFamilyEnum == 2) ? 'unknown' : (processorDeviceFamilyEnum == 3) ? '8086' : (processorDeviceFamilyEnum == 4) ? '80286' : (processorDeviceFamilyEnum == 5) ? 'Intel386' : (processorDeviceFamilyEnum == 6) ? 'Intel486' : (processorDeviceFamilyEnum == 11) ? 'Pentium' : (processorDeviceFamilyEnum == 12) ? 'PentiumPro' : (processorDeviceFamilyEnum == 13) ? 'PentiumII' : (processorDeviceFamilyEnum == 14) ? 'PentiumMMX' : (processorDeviceFamilyEnum == 15) ? 'Celeron' : (processorDeviceFamilyEnum == 16) ? 'PentiumIIXeon' : (processorDeviceFamilyEnum == 17) ? 'PentiumIII' : (processorDeviceFamilyEnum == 18) ? 'PentiumIIIXeon' : (processorDeviceFamilyEnum == 19) ? 'PentiumIIISpeedStep' : (processorDeviceFamilyEnum == 20) ? 'Itanium' : (processorDeviceFamilyEnum == 21) ? 'IntelXeon' : (processorDeviceFamilyEnum == 22) ? 'Pentium4' : (processorDeviceFamilyEnum == 23) ? 'IntelXeonMP' : (processorDeviceFamilyEnum == 24) ? 'IntelItanium2' : (processorDeviceFamilyEnum == 40) ? 'IntelCoreDuo' : (processorDeviceFamilyEnum == 41) ? 'IntelCoreDuoMobile' : (processorDeviceFamilyEnum == 42) ? 'IntelCoreSoloMobile' : (processorDeviceFamilyEnum == 43) ? 'IntelAtom' : (processorDeviceFamilyEnum == 131) ? 'AMDAthlon64' : (processorDeviceFamilyEnum == 132) ? 'AMDOpteron' : (processorDeviceFamilyEnum == 134) ? 'AMDTurion64Mobile' : (processorDeviceFamilyEnum == 190) ? 'IntelCore2' : (processorDeviceFamilyEnum == 191) ? 'IntelCore2Duo' : (processorDeviceFamilyEnum == 194) ? 'IntelCore2Quad' : (processorDeviceFamilyEnum == 198) ? 'IntelCorei7' : (processorDeviceFamilyEnum == 205) ? 'IntelCorei5' : (processorDeviceFamilyEnum == 206) ? 'IntelCorei3' : 'unknown'"
|
||||
protocol: snmp
|
||||
snmp:
|
||||
host: ^_^host^_^
|
||||
port: ^_^port^_^
|
||||
username: ^_^username^_^
|
||||
authPassphrase: ^_^authPassphrase^_^
|
||||
privPassphrase: ^_^privPassphrase^_^
|
||||
privPasswordEncryption: ^_^privPasswordEncryption^_^
|
||||
authPasswordEncryption: ^_^authPasswordEncryption^_^
|
||||
contextName: ^_^contextName^_^
|
||||
timeout: ^_^timeout^_^
|
||||
community: ^_^community^_^
|
||||
version: ^_^snmpVersion^_^
|
||||
operation: walk
|
||||
oids:
|
||||
processorDeviceIndex: 1.3.6.1.4.1.674.10892.5.4.1100.30.1.2
|
||||
processorDeviceLocationName: 1.3.6.1.4.1.674.10892.5.4.1100.30.1.8
|
||||
processorDeviceStatusEnum: 1.3.6.1.4.1.674.10892.5.4.1100.30.1.5
|
||||
processorDeviceSpeed: 1.3.6.1.4.1.674.10892.5.4.1100.30.1.12
|
||||
processorDeviceFamilyEnum: 1.3.6.1.4.1.674.10892.5.4.1100.30.1.10
|
||||
@@ -68,6 +68,9 @@ resourceRole:
|
||||
- /api/bulletin/**===delete===[admin]
|
||||
- /api/sse/**===get===[admin,user]
|
||||
- /api/sse/**===post===[admin,user]
|
||||
- /api/chat/**===get===[admin,user]
|
||||
- /api/chat/**===post===[admin,user]
|
||||
|
||||
# config the resource restful api that need bypass auth protection
|
||||
# rule: api===method
|
||||
# eg: /api/v1/source3===get means /api/v1/source3===get can be access by anyone, no need auth.
|
||||
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* 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.manager.controller;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import com.usthe.sureness.util.SurenessContextHolder;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.manager.service.MetricsFavoriteService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Test case for {@link MetricsFavoriteController}
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MetricsFavoriteControllerTest {
|
||||
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Mock
|
||||
private MetricsFavoriteService metricsFavoriteService;
|
||||
|
||||
@InjectMocks
|
||||
private MetricsFavoriteController metricsFavoriteController;
|
||||
|
||||
private final Long testMonitorId = 1L;
|
||||
private final String testMetricsName = "cpu";
|
||||
private final String testUserId = "testUser";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.mockMvc = MockMvcBuilders.standaloneSetup(metricsFavoriteController)
|
||||
.setMessageConverters(new MappingJackson2HttpMessageConverter())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddMetricsFavoriteSuccess() throws Exception {
|
||||
SubjectSum subjectSum = mock(SubjectSum.class);
|
||||
when(subjectSum.getPrincipal()).thenReturn(testUserId);
|
||||
try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
|
||||
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
|
||||
|
||||
doNothing().when(metricsFavoriteService).addMetricsFavorite(testUserId, testMonitorId, testMetricsName);
|
||||
|
||||
mockMvc.perform(post("/api/metrics/favorite/{monitorId}/{metricsName}", testMonitorId, testMetricsName)
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
|
||||
.andExpect(jsonPath("$.msg").value("Metrics added to favorites successfully"));
|
||||
|
||||
verify(metricsFavoriteService).addMetricsFavorite(testUserId, testMonitorId, testMetricsName);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddMetricsFavoriteAlreadyExists() throws Exception {
|
||||
SubjectSum subjectSum = mock(SubjectSum.class);
|
||||
when(subjectSum.getPrincipal()).thenReturn(testUserId);
|
||||
try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
|
||||
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
|
||||
|
||||
doThrow(new RuntimeException("Metrics favorite already exists: " + testMetricsName))
|
||||
.when(metricsFavoriteService).addMetricsFavorite(testUserId, testMonitorId, testMetricsName);
|
||||
|
||||
mockMvc.perform(post("/api/metrics/favorite/{monitorId}/{metricsName}", testMonitorId, testMetricsName)
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE))
|
||||
.andExpect(jsonPath("$.msg").value("Add failed! Metrics favorite already exists: " + testMetricsName));
|
||||
|
||||
verify(metricsFavoriteService).addMetricsFavorite(testUserId, testMonitorId, testMetricsName);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddMetricsFavoriteMissingParameters() throws Exception {
|
||||
mockMvc.perform(post("/api/metrics/favorite")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isNotFound());
|
||||
|
||||
verify(metricsFavoriteService, never()).addMetricsFavorite(anyString(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveMetricsFavoriteSuccess() throws Exception {
|
||||
SubjectSum subjectSum = mock(SubjectSum.class);
|
||||
when(subjectSum.getPrincipal()).thenReturn(testUserId);
|
||||
try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
|
||||
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
|
||||
|
||||
// Given
|
||||
doNothing().when(metricsFavoriteService).removeMetricsFavorite(testUserId, testMonitorId, testMetricsName);
|
||||
|
||||
// When & Then
|
||||
mockMvc.perform(delete("/api/metrics/favorite/{monitorId}/{metricsName}", testMonitorId, testMetricsName)
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
|
||||
.andExpect(jsonPath("$.msg").value("Metrics removed from favorites successfully"));
|
||||
|
||||
verify(metricsFavoriteService).removeMetricsFavorite(testUserId, testMonitorId, testMetricsName);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveMetricsFavoriteException() throws Exception {
|
||||
SubjectSum subjectSum = mock(SubjectSum.class);
|
||||
when(subjectSum.getPrincipal()).thenReturn(testUserId);
|
||||
try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
|
||||
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
|
||||
|
||||
doThrow(new RuntimeException("Database error"))
|
||||
.when(metricsFavoriteService).removeMetricsFavorite(testUserId, testMonitorId, testMetricsName);
|
||||
|
||||
mockMvc.perform(delete("/api/metrics/favorite/{monitorId}/{metricsName}", testMonitorId, testMetricsName)
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE))
|
||||
.andExpect(jsonPath("$.msg").value("Remove failed! Database error"));
|
||||
|
||||
verify(metricsFavoriteService).removeMetricsFavorite(testUserId, testMonitorId, testMetricsName);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserFavoritedMetricsSuccess() throws Exception {
|
||||
SubjectSum subjectSum = mock(SubjectSum.class);
|
||||
when(subjectSum.getPrincipal()).thenReturn(testUserId);
|
||||
try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
|
||||
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
|
||||
|
||||
Set<String> favoriteMetrics = Set.of("cpu", "memory", "disk");
|
||||
when(metricsFavoriteService.getUserFavoritedMetrics(testUserId, testMonitorId))
|
||||
.thenReturn(favoriteMetrics);
|
||||
|
||||
mockMvc.perform(get("/api/metrics/favorite/{monitorId}", testMonitorId)
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
|
||||
.andExpect(jsonPath("$.data").isArray())
|
||||
.andExpect(jsonPath("$.data.length()").value(3));
|
||||
|
||||
verify(metricsFavoriteService).getUserFavoritedMetrics(testUserId, testMonitorId);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserFavoritedMetricsEmptyResult() throws Exception {
|
||||
SubjectSum subjectSum = mock(SubjectSum.class);
|
||||
when(subjectSum.getPrincipal()).thenReturn(testUserId);
|
||||
try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
|
||||
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
|
||||
|
||||
Set<String> favoriteMetrics = Set.of();
|
||||
when(metricsFavoriteService.getUserFavoritedMetrics(testUserId, testMonitorId))
|
||||
.thenReturn(favoriteMetrics);
|
||||
|
||||
mockMvc.perform(get("/api/metrics/favorite/{monitorId}", testMonitorId)
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
|
||||
.andExpect(jsonPath("$.data").isArray())
|
||||
.andExpect(jsonPath("$.data.length()").value(0));
|
||||
|
||||
verify(metricsFavoriteService).getUserFavoritedMetrics(testUserId, testMonitorId);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserFavoritedMetricsException() throws Exception {
|
||||
SubjectSum subjectSum = mock(SubjectSum.class);
|
||||
when(subjectSum.getPrincipal()).thenReturn(testUserId);
|
||||
try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
|
||||
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
|
||||
|
||||
when(metricsFavoriteService.getUserFavoritedMetrics(testUserId, testMonitorId))
|
||||
.thenThrow(new RuntimeException("Service error"));
|
||||
|
||||
mockMvc.perform(get("/api/metrics/favorite/{monitorId}", testMonitorId)
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.FAIL_CODE))
|
||||
.andExpect(jsonPath("$.msg").value("Failed to get favorited metrics!"));
|
||||
|
||||
verify(metricsFavoriteService).getUserFavoritedMetrics(testUserId, testMonitorId);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserFavoritedMetricsMissingMonitorId() throws Exception {
|
||||
mockMvc.perform(get("/api/metrics/favorite")
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isNotFound());
|
||||
|
||||
verify(metricsFavoriteService, never()).getUserFavoritedMetrics(anyString(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnauthenticatedAccess() throws Exception {
|
||||
try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
|
||||
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(null);
|
||||
mockMvc.perform(post("/api/metrics/favorite/{monitorId}/{metricsName}", testMonitorId, testMetricsName)
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value((int) CommonConstants.LOGIN_FAILED_CODE))
|
||||
.andExpect(jsonPath("$.msg").value("User not authenticated"));
|
||||
|
||||
verify(metricsFavoriteService, never()).addMetricsFavorite(anyString(), anyLong(), anyString());
|
||||
}
|
||||
}
|
||||
}
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* 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.manager.dao;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.hertzbeat.common.entity.manager.MetricsFavorite;
|
||||
import org.apache.hertzbeat.manager.AbstractSpringIntegrationTest;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.annotation.Rollback;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Test case for {@link MetricsFavoriteDao}
|
||||
*/
|
||||
@Transactional
|
||||
class MetricsFavoriteDaoTest extends AbstractSpringIntegrationTest {
|
||||
|
||||
@Resource
|
||||
private MetricsFavoriteDao metricsFavoriteDao;
|
||||
|
||||
private MetricsFavorite testFavorite1;
|
||||
private MetricsFavorite testFavorite2;
|
||||
private MetricsFavorite testFavorite3;
|
||||
|
||||
private final String testCreator1 = "user1";
|
||||
private final String testCreator2 = "user2";
|
||||
private final Long testMonitorId1 = 1L;
|
||||
private final Long testMonitorId2 = 2L;
|
||||
private final String testMetricsName1 = "cpu";
|
||||
private final String testMetricsName2 = "memory";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
testFavorite1 = MetricsFavorite.builder()
|
||||
.creator(testCreator1)
|
||||
.monitorId(testMonitorId1)
|
||||
.metricsName(testMetricsName1)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
testFavorite2 = MetricsFavorite.builder()
|
||||
.creator(testCreator1)
|
||||
.monitorId(testMonitorId1)
|
||||
.metricsName(testMetricsName2)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
testFavorite3 = MetricsFavorite.builder()
|
||||
.creator(testCreator2)
|
||||
.monitorId(testMonitorId2)
|
||||
.metricsName(testMetricsName1)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
metricsFavoriteDao.deleteAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveAndFindById() {
|
||||
MetricsFavorite saved = metricsFavoriteDao.saveAndFlush(testFavorite1);
|
||||
|
||||
assertNotNull(saved.getId());
|
||||
Optional<MetricsFavorite> found = metricsFavoriteDao.findById(saved.getId());
|
||||
assertTrue(found.isPresent());
|
||||
assertEquals(testCreator1, found.get().getCreator());
|
||||
assertEquals(testMonitorId1, found.get().getMonitorId());
|
||||
assertEquals(testMetricsName1, found.get().getMetricsName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindByCreatorAndMonitorIdAndMetricsName() {
|
||||
metricsFavoriteDao.saveAndFlush(testFavorite1);
|
||||
|
||||
Optional<MetricsFavorite> found = metricsFavoriteDao
|
||||
.findByCreatorAndMonitorIdAndMetricsName(testCreator1, testMonitorId1, testMetricsName1);
|
||||
|
||||
assertTrue(found.isPresent());
|
||||
assertEquals(testCreator1, found.get().getCreator());
|
||||
assertEquals(testMonitorId1, found.get().getMonitorId());
|
||||
assertEquals(testMetricsName1, found.get().getMetricsName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindByCreatorAndMonitorIdAndMetricsNameNotFound() {
|
||||
Optional<MetricsFavorite> found = metricsFavoriteDao.findByCreatorAndMonitorIdAndMetricsName("nonexistent", 999L, "nonexistent");
|
||||
assertFalse(found.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindByCreatorAndMonitorId() {
|
||||
metricsFavoriteDao.saveAndFlush(testFavorite1);
|
||||
metricsFavoriteDao.saveAndFlush(testFavorite2);
|
||||
metricsFavoriteDao.saveAndFlush(testFavorite3);
|
||||
|
||||
List<MetricsFavorite> found = metricsFavoriteDao.findByCreatorAndMonitorId(testCreator1, testMonitorId1);
|
||||
|
||||
assertNotNull(found);
|
||||
assertEquals(2, found.size());
|
||||
assertTrue(found.stream().allMatch(f -> f.getCreator().equals(testCreator1)));
|
||||
assertTrue(found.stream().allMatch(f -> f.getMonitorId().equals(testMonitorId1)));
|
||||
assertTrue(found.stream().anyMatch(f -> f.getMetricsName().equals(testMetricsName1)));
|
||||
assertTrue(found.stream().anyMatch(f -> f.getMetricsName().equals(testMetricsName2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindByCreatorAndMonitorIdEmptyResult() {
|
||||
List<MetricsFavorite> found = metricsFavoriteDao.findByCreatorAndMonitorId("nonexistent", 999L);
|
||||
assertNotNull(found);
|
||||
assertTrue(found.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteByUserIdAndMonitorIdAndMetricsName() {
|
||||
MetricsFavorite saved = metricsFavoriteDao.saveAndFlush(testFavorite1);
|
||||
|
||||
assertTrue(metricsFavoriteDao.findById(saved.getId()).isPresent());
|
||||
|
||||
metricsFavoriteDao.deleteByUserIdAndMonitorIdAndMetricsName(testCreator1, testMonitorId1, testMetricsName1);
|
||||
assertFalse(metricsFavoriteDao.findById(saved.getId()).isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteByUserIdAndMonitorIdAndMetricsNameNotExists() {
|
||||
// When - Should not throw exception even if record doesn't exist
|
||||
assertDoesNotThrow(() -> {
|
||||
metricsFavoriteDao.deleteByUserIdAndMonitorIdAndMetricsName("nonexistent", 999L, "nonexistent");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteFavoritesByMonitorIdIn() {
|
||||
MetricsFavorite saved1 = metricsFavoriteDao.saveAndFlush(testFavorite1);
|
||||
MetricsFavorite saved2 = metricsFavoriteDao.saveAndFlush(testFavorite2);
|
||||
MetricsFavorite saved3 = metricsFavoriteDao.saveAndFlush(testFavorite3);
|
||||
|
||||
assertEquals(3, metricsFavoriteDao.findAll().size());
|
||||
|
||||
Set<Long> monitorIds = Set.of(testMonitorId1, testMonitorId2);
|
||||
metricsFavoriteDao.deleteFavoritesByMonitorIdIn(monitorIds);
|
||||
|
||||
assertEquals(0, metricsFavoriteDao.findAll().size());
|
||||
assertFalse(metricsFavoriteDao.findById(saved1.getId()).isPresent());
|
||||
assertFalse(metricsFavoriteDao.findById(saved2.getId()).isPresent());
|
||||
assertFalse(metricsFavoriteDao.findById(saved3.getId()).isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteFavoritesByMonitorIdInPartialDelete() {
|
||||
MetricsFavorite saved1 = metricsFavoriteDao.saveAndFlush(testFavorite1);
|
||||
MetricsFavorite saved3 = metricsFavoriteDao.saveAndFlush(testFavorite3);
|
||||
|
||||
Set<Long> monitorIds = Set.of(testMonitorId1);
|
||||
metricsFavoriteDao.deleteFavoritesByMonitorIdIn(monitorIds);
|
||||
|
||||
assertEquals(1, metricsFavoriteDao.findAll().size());
|
||||
assertFalse(metricsFavoriteDao.findById(saved1.getId()).isPresent());
|
||||
assertTrue(metricsFavoriteDao.findById(saved3.getId()).isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteFavoritesByMonitorIdInNonExistentIds() {
|
||||
metricsFavoriteDao.saveAndFlush(testFavorite1);
|
||||
|
||||
Set<Long> nonExistentIds = Set.of(999L, 1000L);
|
||||
metricsFavoriteDao.deleteFavoritesByMonitorIdIn(nonExistentIds);
|
||||
|
||||
assertEquals(1, metricsFavoriteDao.findAll().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Rollback(false)
|
||||
void testUniqueConstraint() {
|
||||
metricsFavoriteDao.saveAndFlush(testFavorite1);
|
||||
|
||||
MetricsFavorite duplicate = MetricsFavorite.builder()
|
||||
.creator(testCreator1)
|
||||
.monitorId(testMonitorId1)
|
||||
.metricsName(testMetricsName1)
|
||||
.createTime(LocalDateTime.now()).build();
|
||||
|
||||
assertThrows(java.sql.SQLException.class, () -> {
|
||||
metricsFavoriteDao.saveAndFlush(duplicate);
|
||||
});
|
||||
|
||||
// Clean up the test data
|
||||
metricsFavoriteDao.deleteAll();
|
||||
}
|
||||
}
|
||||
+14
-3
@@ -22,7 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.when;
|
||||
import java.util.ArrayList;
|
||||
@@ -31,6 +31,9 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import org.apache.hertzbeat.alert.dao.AlertDefineBindDao;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.entity.job.Job;
|
||||
@@ -651,8 +654,16 @@ class MonitorServiceTest {
|
||||
|
||||
@Test
|
||||
void getMonitors() {
|
||||
doReturn(Page.empty()).when(monitorDao).findAll(any(Specification.class), any(PageRequest.class));
|
||||
assertNotNull(monitorService.getMonitors(null, null, null, null, "gmtCreate", "desc", 1, 1, null));
|
||||
when(monitorDao.findAll(any(Specification.class), any(PageRequest.class))).thenAnswer((invocation)->{
|
||||
Specification<Monitor> spec = invocation.getArgument(0);
|
||||
CriteriaBuilder cb = mock(CriteriaBuilder.class);
|
||||
CriteriaQuery<?> query = mock(CriteriaQuery.class);
|
||||
Root<Monitor> root = mock(Root.class);
|
||||
spec.toPredicate(root, query, cb);
|
||||
return Page.empty();
|
||||
});
|
||||
assertNotNull(monitorService.getMonitors(null, null, "9.111", null, "gmtCreate", "desc", 1, 1, null));
|
||||
assertNotNull(monitorService.getMonitors(null, null, "9", null, "gmtCreate", "desc", 1, 1, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* 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.manager.service.impl;
|
||||
|
||||
import org.apache.hertzbeat.common.entity.manager.MetricsFavorite;
|
||||
import org.apache.hertzbeat.manager.dao.MetricsFavoriteDao;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Test case for {@link MetricsFavoriteServiceImpl}
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MetricsFavoriteServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private MetricsFavoriteDao metricsFavoriteDao;
|
||||
|
||||
@InjectMocks
|
||||
private MetricsFavoriteServiceImpl metricsFavoriteService;
|
||||
|
||||
private MetricsFavorite testFavorite;
|
||||
private final String testCreator = "testUser";
|
||||
private final Long testMonitorId = 1L;
|
||||
private final String testMetricsName = "cpu";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
testFavorite = MetricsFavorite.builder()
|
||||
.id(1L)
|
||||
.creator(testCreator)
|
||||
.monitorId(testMonitorId)
|
||||
.metricsName(testMetricsName)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddMetricsFavoriteSuccess() {
|
||||
when(metricsFavoriteDao.findByCreatorAndMonitorIdAndMetricsName(testCreator, testMonitorId, testMetricsName))
|
||||
.thenReturn(Optional.empty());
|
||||
when(metricsFavoriteDao.save(any(MetricsFavorite.class))).thenReturn(testFavorite);
|
||||
|
||||
assertDoesNotThrow(() -> metricsFavoriteService.addMetricsFavorite(testCreator, testMonitorId, testMetricsName));
|
||||
|
||||
verify(metricsFavoriteDao).findByCreatorAndMonitorIdAndMetricsName(testCreator, testMonitorId, testMetricsName);
|
||||
verify(metricsFavoriteDao).save(any(MetricsFavorite.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddMetricsFavoriteAlreadyExists() {
|
||||
when(metricsFavoriteDao.findByCreatorAndMonitorIdAndMetricsName(testCreator, testMonitorId, testMetricsName))
|
||||
.thenReturn(Optional.of(testFavorite));
|
||||
|
||||
RuntimeException exception = assertThrows(RuntimeException.class,
|
||||
() -> metricsFavoriteService.addMetricsFavorite(testCreator, testMonitorId, testMetricsName));
|
||||
|
||||
assertEquals("Metrics favorite already exists: " + testMetricsName, exception.getMessage());
|
||||
verify(metricsFavoriteDao).findByCreatorAndMonitorIdAndMetricsName(testCreator, testMonitorId, testMetricsName);
|
||||
verify(metricsFavoriteDao, never()).save(any(MetricsFavorite.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveMetricsFavorite() {
|
||||
metricsFavoriteService.removeMetricsFavorite(testCreator, testMonitorId, testMetricsName);
|
||||
verify(metricsFavoriteDao).deleteByUserIdAndMonitorIdAndMetricsName(testCreator, testMonitorId, testMetricsName);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserFavoritedMetricsWithData() {
|
||||
MetricsFavorite favorite1 = MetricsFavorite.builder()
|
||||
.creator(testCreator)
|
||||
.monitorId(testMonitorId)
|
||||
.metricsName("cpu")
|
||||
.build();
|
||||
MetricsFavorite favorite2 = MetricsFavorite.builder()
|
||||
.creator(testCreator)
|
||||
.monitorId(testMonitorId)
|
||||
.metricsName("memory")
|
||||
.build();
|
||||
List<MetricsFavorite> favorites = Arrays.asList(favorite1, favorite2);
|
||||
|
||||
when(metricsFavoriteDao.findByCreatorAndMonitorId(testCreator, testMonitorId))
|
||||
.thenReturn(favorites);
|
||||
|
||||
Set<String> result = metricsFavoriteService.getUserFavoritedMetrics(testCreator, testMonitorId);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(2, result.size());
|
||||
assertTrue(result.contains("cpu"));
|
||||
assertTrue(result.contains("memory"));
|
||||
verify(metricsFavoriteDao).findByCreatorAndMonitorId(testCreator, testMonitorId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserFavoritedMetricsEmptyData() {
|
||||
when(metricsFavoriteDao.findByCreatorAndMonitorId(testCreator, testMonitorId))
|
||||
.thenReturn(List.of());
|
||||
|
||||
Set<String> result = metricsFavoriteService.getUserFavoritedMetrics(testCreator, testMonitorId);
|
||||
|
||||
assertNotNull(result);
|
||||
assertTrue(result.isEmpty());
|
||||
verify(metricsFavoriteDao).findByCreatorAndMonitorId(testCreator, testMonitorId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserFavoritedMetricsNullData() {
|
||||
when(metricsFavoriteDao.findByCreatorAndMonitorId(testCreator, testMonitorId))
|
||||
.thenReturn(null);
|
||||
|
||||
Set<String> result = metricsFavoriteService.getUserFavoritedMetrics(testCreator, testMonitorId);
|
||||
|
||||
assertNotNull(result);
|
||||
assertTrue(result.isEmpty());
|
||||
verify(metricsFavoriteDao).findByCreatorAndMonitorId(testCreator, testMonitorId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserFavoritedMetricsFilterBlankNames() {
|
||||
MetricsFavorite favorite1 = MetricsFavorite.builder()
|
||||
.creator(testCreator)
|
||||
.monitorId(testMonitorId)
|
||||
.metricsName("cpu")
|
||||
.build();
|
||||
MetricsFavorite favorite2 = MetricsFavorite.builder()
|
||||
.creator(testCreator)
|
||||
.monitorId(testMonitorId)
|
||||
.metricsName("")
|
||||
.build();
|
||||
MetricsFavorite favorite3 = MetricsFavorite.builder()
|
||||
.creator(testCreator)
|
||||
.monitorId(testMonitorId)
|
||||
.metricsName(null)
|
||||
.build();
|
||||
List<MetricsFavorite> favorites = Arrays.asList(favorite1, favorite2, favorite3);
|
||||
|
||||
when(metricsFavoriteDao.findByCreatorAndMonitorId(testCreator, testMonitorId))
|
||||
.thenReturn(favorites);
|
||||
|
||||
Set<String> result = metricsFavoriteService.getUserFavoritedMetrics(testCreator, testMonitorId);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
assertTrue(result.contains("cpu"));
|
||||
verify(metricsFavoriteDao).findByCreatorAndMonitorId(testCreator, testMonitorId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteFavoritesByMonitorIdIn() {
|
||||
Set<Long> monitorIds = Set.of(1L, 2L, 3L);
|
||||
|
||||
metricsFavoriteService.deleteFavoritesByMonitorIdIn(monitorIds);
|
||||
|
||||
verify(metricsFavoriteDao).deleteFavoritesByMonitorIdIn(monitorIds);
|
||||
}
|
||||
}
|
||||
@@ -96,4 +96,4 @@
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
</project>
|
||||
|
||||
+23
-23
@@ -39,28 +39,28 @@
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>${maven-assembly-plugin.version}</version>
|
||||
<configuration>
|
||||
<descriptors>
|
||||
<descriptor>src/main/resources/assembly/assembly.xml</descriptor>
|
||||
</descriptors>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>${maven-assembly-plugin.version}</version>
|
||||
<configuration>
|
||||
<descriptors>
|
||||
<descriptor>src/main/resources/assembly/assembly.xml</descriptor>
|
||||
</descriptors>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -56,5 +56,4 @@
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
</project>
|
||||
|
||||
@@ -83,6 +83,12 @@
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- QuestDB -->
|
||||
<dependency>
|
||||
<groupId>org.questdb</groupId>
|
||||
<artifactId>questdb</artifactId>
|
||||
<version>${questdb.version}</version>
|
||||
</dependency>
|
||||
<!-- influxdb Here, support for version 1.7, use influxdb-java, Full support for version 2.x requires influxdb-client-java -->
|
||||
<dependency>
|
||||
<groupId>org.influxdb</groupId>
|
||||
|
||||
+2
@@ -44,6 +44,8 @@ public interface WarehouseConstants {
|
||||
String VM = "victoria-metrics";
|
||||
|
||||
String VM_CLUSTER = "victoria-metrics.cluster";
|
||||
|
||||
String QUEST_DB = "questdb";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+412
@@ -0,0 +1,412 @@
|
||||
/*
|
||||
* 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.warehouse.store.history.tsdb.questdb;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import io.questdb.client.Sender;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.ConnectionPool;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.constants.MetricDataConstants;
|
||||
import org.apache.hertzbeat.common.constants.NetworkConstants;
|
||||
import org.apache.hertzbeat.common.entity.arrow.RowWrapper;
|
||||
import org.apache.hertzbeat.common.entity.dto.Value;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.apache.hertzbeat.warehouse.store.history.tsdb.AbstractHistoryDataStorage;
|
||||
import org.apache.http.ssl.SSLContexts;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* HistoryQuestdbDataStorage class
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(prefix = "warehouse.store.questdb", name = "enabled", havingValue = "true")
|
||||
@Slf4j
|
||||
public class QuestdbDataStorage extends AbstractHistoryDataStorage {
|
||||
|
||||
private static final String QUERY_HISTORY_SQL = "SELECT timestamp AS ts, instance, %s AS value FROM \"%s\" WHERE timestamp >= %s ORDER BY timestamp DESC";
|
||||
|
||||
private static final String QUERY_HISTORY_SQL_WITH_INSTANCE = "SELECT timestamp AS ts, instance, %s AS value FROM \"%s\" WHERE instance = '%s' AND timestamp >= %s ORDER BY timestamp DESC";
|
||||
|
||||
private static final String QUERY_HISTORY_INTERVAL_WITH_INSTANCE_SQL =
|
||||
"SELECT timestamp AS ts, first(%s) AS origin, avg(%s) AS mean, max(%s) AS max, min(%s) AS min FROM \"%s\" WHERE instance = '%s' AND timestamp >= %s SAMPLE BY 4h";
|
||||
|
||||
private static final String QUERY_INSTANCE_SQL = "SELECT DISTINCT instance FROM \"%s\"";
|
||||
|
||||
private Sender sender;
|
||||
|
||||
private OkHttpClient client;
|
||||
|
||||
private String queryBaseUrl;
|
||||
|
||||
private QuestdbProperties questdbProperties;
|
||||
|
||||
public QuestdbDataStorage(QuestdbProperties questdbProperties) {
|
||||
this.questdbProperties = questdbProperties;
|
||||
this.initQuestDb(questdbProperties);
|
||||
}
|
||||
|
||||
public void initQuestDb(QuestdbProperties questdbProperties) {
|
||||
String ilpAddress = questdbProperties.url(); // e.g., "localhost:9009"
|
||||
this.sender = Sender.builder(Sender.Transport.HTTP)
|
||||
.address(ilpAddress)
|
||||
.httpUsernamePassword(questdbProperties.username(), questdbProperties.password())
|
||||
.build();
|
||||
|
||||
// Parse host and set query base URL (assuming HTTP port is 9000)
|
||||
String[] parts = ilpAddress.split(":");
|
||||
String host = parts[0];
|
||||
String queryPort = "9000"; // Default QuestDB HTTP port
|
||||
this.queryBaseUrl = "http://" + host + ":" + queryPort + "/exec?query=";
|
||||
|
||||
this.client = new OkHttpClient.Builder()
|
||||
.readTimeout(NetworkConstants.HttpClientConstants.READ_TIME_OUT, TimeUnit.SECONDS)
|
||||
.writeTimeout(NetworkConstants.HttpClientConstants.WRITE_TIME_OUT, TimeUnit.SECONDS)
|
||||
.connectTimeout(NetworkConstants.HttpClientConstants.CONNECT_TIME_OUT, TimeUnit.SECONDS)
|
||||
.connectionPool(new ConnectionPool(
|
||||
NetworkConstants.HttpClientConstants.MAX_IDLE_CONNECTIONS,
|
||||
NetworkConstants.HttpClientConstants.KEEP_ALIVE_TIMEOUT,
|
||||
TimeUnit.SECONDS)
|
||||
).sslSocketFactory(defaultSslSocketFactory(), defaultTrustManager())
|
||||
.hostnameVerifier(noopHostnameVerifier())
|
||||
.retryOnConnectionFailure(true)
|
||||
.build();
|
||||
|
||||
this.serverAvailable = this.checkConnection();
|
||||
}
|
||||
|
||||
private boolean checkConnection() {
|
||||
// Test query to check if server is available
|
||||
Map<String, Object> result = executeQuery("SELECT 1");
|
||||
return result != null && result.containsKey("dataset");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void saveData(CollectRep.MetricsData metricsData) {
|
||||
if (!isServerAvailable() || metricsData.getCode() != CollectRep.Code.SUCCESS || metricsData.getValues().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String table = this.generateTable(metricsData.getApp(), metricsData.getMetrics(), metricsData.getId());
|
||||
|
||||
try {
|
||||
RowWrapper rowWrapper = metricsData.readRow();
|
||||
|
||||
while (rowWrapper.hasNextRow()) {
|
||||
rowWrapper = rowWrapper.nextRow();
|
||||
// Wrap the construction of each row in a try-catch block
|
||||
// to prevent one bad row from corrupting the sender's state.
|
||||
try {
|
||||
// 1. Set the table for the new row.
|
||||
sender.table(table);
|
||||
// 2. Process and write all symbols FIRST.
|
||||
Map<String, String> labels = Maps.newHashMapWithExpectedSize(8);
|
||||
rowWrapper.cellStream()
|
||||
.filter(cell -> cell.getMetadataAsBoolean(MetricDataConstants.LABEL))
|
||||
.forEach(cell -> labels.put(cell.getField().getName(), cell.getValue()));
|
||||
if (!labels.isEmpty()) {
|
||||
sender.symbol("instance", JsonUtil.toJson(labels));
|
||||
} else {
|
||||
sender.symbol("instance", metricsData.getApp()
|
||||
+ "_" + metricsData.getMetrics());
|
||||
}
|
||||
|
||||
// 3. Now, process and write all other columns (fields).
|
||||
rowWrapper.cellStream().forEach(cell -> {
|
||||
if (CommonConstants.NULL_VALUE.equals(cell.getValue())) {
|
||||
return;
|
||||
}
|
||||
String fieldName = cell.getField().getName();
|
||||
String fieldValue = cell.getValue();
|
||||
Byte type = cell.getMetadataAsByte(MetricDataConstants.TYPE);
|
||||
|
||||
if (type == CommonConstants.TYPE_NUMBER) {
|
||||
sender.doubleColumn(fieldName, Double.parseDouble(fieldValue));
|
||||
} else if (type == CommonConstants.TYPE_STRING) {
|
||||
sender.stringColumn(fieldName, fieldValue);
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Finally, set the timestamp to commit the row.
|
||||
sender.atNow();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[warehouse questdb]--Could not process a row, cancelling it. Error: {}", e.getMessage());
|
||||
// IMPORTANT: Cancel the partially built row to reset the sender's state.
|
||||
sender.cancelRow();
|
||||
}
|
||||
}
|
||||
sender.flush();
|
||||
} catch (Exception e) {
|
||||
log.error("[warehouse questdb]--Error during batch save: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<Value>> getHistoryMetricData(Long monitorId, String app, String metrics, String metric, String label, String history) {
|
||||
String table = this.generateTable(app, metrics, monitorId);
|
||||
String dateAdd = getDateAdd(history);
|
||||
String selectSql = label == null ? String.format(QUERY_HISTORY_SQL, metric, table, dateAdd)
|
||||
: String.format(QUERY_HISTORY_SQL_WITH_INSTANCE, metric, table, label.replace("'", "\\'"), dateAdd);
|
||||
Map<String, List<Value>> instanceValueMap = new HashMap<>(8);
|
||||
try {
|
||||
Map<String, Object> selectResult = executeQuery(selectSql);
|
||||
if (selectResult == null || !selectResult.containsKey("dataset")) {
|
||||
return instanceValueMap;
|
||||
}
|
||||
List<Map<String, String>> columns = (List<Map<String, String>>) selectResult.get("columns");
|
||||
List<List<Object>> dataset = (List<List<Object>>) selectResult.get("dataset");
|
||||
|
||||
Map<String, Integer> colMap = new HashMap<>();
|
||||
for (int i = 0; i < columns.size(); i++) {
|
||||
colMap.put(columns.get(i).get("name"), i);
|
||||
}
|
||||
int tsIdx = colMap.get("ts");
|
||||
int instanceIdx = colMap.get("instance");
|
||||
int valueIdx = colMap.get("value");
|
||||
|
||||
for (List<Object> row : dataset) {
|
||||
String tsStr = (String) row.get(tsIdx);
|
||||
long time = Instant.parse(tsStr).toEpochMilli();
|
||||
String instanceValue = row.get(instanceIdx) == null ? "" : (String) row.get(instanceIdx);
|
||||
Object valObj = row.get(valueIdx);
|
||||
String strValue = valObj == null ? null : this.parseDoubleValue(valObj.toString());
|
||||
if (strValue == null) {
|
||||
continue;
|
||||
}
|
||||
List<Value> valueList = instanceValueMap.computeIfAbsent(instanceValue, k -> new LinkedList<>());
|
||||
valueList.add(new Value(strValue, time));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("select history metric data in questdb error, sql:{}, msg: {}", selectSql, e.getMessage());
|
||||
}
|
||||
return instanceValueMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<Value>> getHistoryIntervalMetricData(Long monitorId, String app, String metrics, String metric, String label, String history) {
|
||||
String table = this.generateTable(app, metrics, monitorId);
|
||||
String dateAdd = getDateAdd(history);
|
||||
Map<String, List<Value>> instanceValueMap = new HashMap<>(8);
|
||||
Set<String> instances = new HashSet<>(8);
|
||||
if (label != null) {
|
||||
instances.add(label);
|
||||
}
|
||||
if (instances.isEmpty()) {
|
||||
// query the instance
|
||||
String queryInstanceSql = String.format(QUERY_INSTANCE_SQL, table);
|
||||
Map<String, Object> instanceQueryResult = executeQuery(queryInstanceSql);
|
||||
if (instanceQueryResult != null && instanceQueryResult.containsKey("dataset")) {
|
||||
List<List<Object>> dataset = (List<List<Object>>) instanceQueryResult.get("dataset");
|
||||
for (List<Object> row : dataset) {
|
||||
if (!row.isEmpty() && row.get(0) != null) {
|
||||
instances.add(row.get(0).toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (instances.isEmpty()) {
|
||||
instances.add("");
|
||||
}
|
||||
for (String instanceValue : instances) {
|
||||
String selectSql = String.format(QUERY_HISTORY_INTERVAL_WITH_INSTANCE_SQL, metric, metric, metric, metric, table, instanceValue.replace("'", "\\'"), dateAdd);
|
||||
Map<String, Object> selectResult = executeQuery(selectSql);
|
||||
if (selectResult == null || !selectResult.containsKey("dataset")) {
|
||||
continue;
|
||||
}
|
||||
List<Map<String, String>> columns = (List<Map<String, String>>) selectResult.get("columns");
|
||||
List<List<Object>> dataset = (List<List<Object>>) selectResult.get("dataset");
|
||||
|
||||
Map<String, Integer> colMap = new HashMap<>();
|
||||
for (int i = 0; i < columns.size(); i++) {
|
||||
colMap.put(columns.get(i).get("name"), i);
|
||||
}
|
||||
int tsIdx = colMap.get("ts");
|
||||
int originIdx = colMap.get("origin");
|
||||
int meanIdx = colMap.get("mean");
|
||||
int maxIdx = colMap.get("max");
|
||||
int minIdx = colMap.get("min");
|
||||
|
||||
for (List<Object> row : dataset) {
|
||||
String tsStr = (String) row.get(tsIdx);
|
||||
long time = Instant.parse(tsStr).toEpochMilli();
|
||||
Value.ValueBuilder valueBuilder = Value.builder().time(time);
|
||||
|
||||
Object originObj = row.get(originIdx);
|
||||
if (originObj != null) {
|
||||
valueBuilder.origin(this.parseDoubleValue(originObj.toString()));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
Object meanObj = row.get(meanIdx);
|
||||
if (meanObj != null) {
|
||||
valueBuilder.mean(this.parseDoubleValue(meanObj.toString()));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
Object maxObj = row.get(maxIdx);
|
||||
if (maxObj != null) {
|
||||
valueBuilder.max(this.parseDoubleValue(maxObj.toString()));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
Object minObj = row.get(minIdx);
|
||||
if (minObj != null) {
|
||||
valueBuilder.min(this.parseDoubleValue(minObj.toString()));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
List<Value> valueList = instanceValueMap.computeIfAbsent(instanceValue, k -> new LinkedList<>());
|
||||
valueList.add(valueBuilder.build());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("select history interval metric data in questdb error, msg: {}", e.getMessage());
|
||||
}
|
||||
return instanceValueMap;
|
||||
}
|
||||
|
||||
private Map<String, Object> executeQuery(String sql) {
|
||||
try {
|
||||
String encodedSql = URLEncoder.encode(sql, StandardCharsets.UTF_8);
|
||||
String url = queryBaseUrl + encodedSql + "×tamptype=rfc3339";
|
||||
String authHeader = "Basic " + Base64.getEncoder().encodeToString(
|
||||
(questdbProperties.username() + ":" + questdbProperties.password())
|
||||
.getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("Authorization", authHeader)
|
||||
.get()
|
||||
.build();
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
if (!response.isSuccessful()) {
|
||||
log.error("QuestDB query failed: {} - {}", response.code(), response.message());
|
||||
return null;
|
||||
}
|
||||
String body = response.body().string();
|
||||
return JsonUtil.fromJson(body, Map.class);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error executing QuestDB query: {} - {}", sql, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String getDateAdd(String history) {
|
||||
history = history.toLowerCase();
|
||||
char unitChar = history.charAt(history.length() - 1);
|
||||
int count = Integer.parseInt(history.substring(0, history.length() - 1));
|
||||
String unit;
|
||||
switch (unitChar) {
|
||||
case 'd':
|
||||
unit = "d";
|
||||
break;
|
||||
case 'h':
|
||||
unit = "h";
|
||||
break;
|
||||
case 'm': // minute
|
||||
unit = "m";
|
||||
break;
|
||||
case 's':
|
||||
unit = "s";
|
||||
break;
|
||||
default: throw new IllegalArgumentException("Invalid history unit: " + unitChar);
|
||||
}
|
||||
return String.format("dateadd('%s', %d, now())", unit, -count);
|
||||
}
|
||||
|
||||
private String generateTable(String app, String metrics, Long monitorId) {
|
||||
return app + "_" + metrics + "_" + monitorId;
|
||||
}
|
||||
|
||||
private String parseDoubleValue(String value) {
|
||||
return (new BigDecimal(value)).setScale(4, RoundingMode.HALF_UP).stripTrailingZeros().toPlainString();
|
||||
}
|
||||
|
||||
private static X509TrustManager defaultTrustManager() {
|
||||
return new X509TrustManager() {
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] certs, String authType) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] certs, String authType) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static SSLSocketFactory defaultSslSocketFactory() {
|
||||
try {
|
||||
SSLContext sslContext = SSLContexts.createDefault();
|
||||
sslContext.init(null, new TrustManager[]{
|
||||
defaultTrustManager()
|
||||
}, new SecureRandom());
|
||||
return sslContext.getSocketFactory();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static HostnameVerifier noopHostnameVerifier() {
|
||||
return (s, sslSession) -> true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
if (this.sender != null) {
|
||||
this.sender.close();
|
||||
}
|
||||
if (this.client != null) {
|
||||
this.client.dispatcher().executorService().shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.warehouse.store.history.tsdb.questdb;
|
||||
|
||||
import org.apache.hertzbeat.common.constants.ConfigConstants;
|
||||
import org.apache.hertzbeat.common.constants.SignConstants;
|
||||
import org.apache.hertzbeat.warehouse.constants.WarehouseConstants;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.bind.DefaultValue;
|
||||
|
||||
/**
|
||||
* QuestDB configuration information
|
||||
*/
|
||||
|
||||
@ConfigurationProperties(prefix = ConfigConstants.FunctionModuleConstants.WAREHOUSE
|
||||
+ SignConstants.DOT
|
||||
+ WarehouseConstants.STORE
|
||||
+ SignConstants.DOT
|
||||
+ WarehouseConstants.HistoryName.QUEST_DB)
|
||||
public record QuestdbProperties(@DefaultValue("false") boolean enabled,
|
||||
String url,
|
||||
String username,
|
||||
String password) {
|
||||
}
|
||||
@@ -31,7 +31,7 @@ tags: [opensource, apache]
|
||||
|
||||
## 2. Incubation Journey and Milestones
|
||||
|
||||
#### **I. Incubation Journey**
|
||||
### **I. Incubation Journey**
|
||||
|
||||
- **April 2024**:
|
||||
HertzBeat officially applied to join the Apache Incubator and became an Apache Incubator project.
|
||||
@@ -54,14 +54,14 @@ During the incubation period, Hertzbeat maintained an active pace of research, d
|
||||
|
||||
- Released 5 ASF-compliant versions (1.6.0 - 1.7.2)
|
||||
- Key feature enhancements include:
|
||||
- **Multi-protocol expansion** - Added support for key components or protocols such as Ipmi, PLC, NVIDIA, Redfish.
|
||||
- **Intelligent alert center** - Implemented multi-expression alerts, periodic thresholds, and alert suppression/silencing/dispatch functions.
|
||||
- **Dynamic service discovery** - Achieved automatic task discovery through http_sd and Zookeeper/Nacos.
|
||||
- **Cloud service integration** - Supported integration with alert sources from Alibaba Cloud SLS, Huawei Cloud, and Volcano Engine.
|
||||
- **AI capability enhancement** - Integrated AI platforms such as OpenAI.
|
||||
- **Storage optimization** - Upgraded storage performance for VictoriaMetrics and GreptimeDB.
|
||||
- **Internationalization support** - Added multi-language interfaces, including Japanese/Traditional Chinese.
|
||||
- **Data processing enhancement** - Supported SQL/PromQL expression syntax analysis, refactoring data flow with Apache Arrow.
|
||||
- **Multi-protocol expansion** - Added support for key components or protocols such as Ipmi, PLC, NVIDIA, Redfish.
|
||||
- **Intelligent alert center** - Implemented multi-expression alerts, periodic thresholds, and alert suppression/silencing/dispatch functions.
|
||||
- **Dynamic service discovery** - Achieved automatic task discovery through http_sd and Zookeeper/Nacos.
|
||||
- **Cloud service integration** - Supported integration with alert sources from Alibaba Cloud SLS, Huawei Cloud, and Volcano Engine.
|
||||
- **AI capability enhancement** - Integrated AI platforms such as OpenAI.
|
||||
- **Storage optimization** - Upgraded storage performance for VictoriaMetrics and GreptimeDB.
|
||||
- **Internationalization support** - Added multi-language interfaces, including Japanese/Traditional Chinese.
|
||||
- **Data processing enhancement** - Supported SQL/PromQL expression syntax analysis, refactoring data flow with Apache Arrow.
|
||||
|
||||
The continuous iteration of these versions fully demonstrates Hertzbeat's dedication to technology and its high regard for user needs. Each version release is a solid step in Hertzbeat's growth, bringing users a more powerful and user-friendly monitoring experience.
|
||||
|
||||
@@ -74,31 +74,31 @@ During the Apache incubation period, Hertzbeat strictly adhered to the Apache Wa
|
||||
- **Consensus-driven decision-making**: For key decisions such as adding or removing features and choosing technical routes, the community engages in thorough discussions, weighing various factors. If a complete consensus cannot be reached, voting is used to assist, ensuring that the project's direction aligns with the overall interests of the community.
|
||||
|
||||
- **Contributor Growth**:
|
||||
- During the incubation period, **13 new Committers** were added, including **3 nominated PPMC members**, and the number of contributors reached 275. The core team also covers multiple time zones in China, the US, and Europe.
|
||||
- GitHub **Stars surpassed 6K+**, with an average of **30+** monthly active developers.
|
||||
- During the incubation period, **13 new Committers** were added, including **3 nominated PPMC members**, and the number of contributors reached 275. The core team also covers multiple time zones in China, the US, and Europe.
|
||||
- GitHub **Stars surpassed 6K+**, with an average of **30+** monthly active developers.
|
||||
- **User Ecosystem**:
|
||||
- **Application Areas**: Since its incubation, Apache HertzBeat™ has been successfully implemented in various industry scenarios due to its lightweight, agentless, and highly scalable features, becoming a new choice for enterprise monitoring infrastructure.
|
||||
- **Developer Ecosystem**: The community has contributed a large number of monitoring templates covering mainstream technology stacks such as Kubernetes, Docker, Redis, and MySQL, significantly reducing the cost of monitoring configuration.
|
||||
- **Application Areas**: Since its incubation, Apache HertzBeat™ has been successfully implemented in various industry scenarios due to its lightweight, agentless, and highly scalable features, becoming a new choice for enterprise monitoring infrastructure.
|
||||
- **Developer Ecosystem**: The community has contributed a large number of monitoring templates covering mainstream technology stacks such as Kubernetes, Docker, Redis, and MySQL, significantly reducing the cost of monitoring configuration.
|
||||
|
||||
## 3. Future Outlook and Plans
|
||||
|
||||
Becoming an Apache Top-Level Project is a milestone, but it is also a new beginning. The Apache HertzBeat™ community will continue to uphold the Apache Foundation's "community over code" philosophy and evolve around the following core directions, striving to become a world-leading open-source observability platform:
|
||||
|
||||
1. AI and Intelligent Capabilities
|
||||
- Explore the application of AI in monitoring data analysis, anomaly detection, and intelligent alerting.
|
||||
- Integrate more AI models and services to enhance the user experience.
|
||||
- Explore the application of AI in monitoring data analysis, anomaly detection, and intelligent alerting.
|
||||
- Integrate more AI models and services to enhance the user experience.
|
||||
|
||||
2. Enhance Product Performance and Scalability
|
||||
- Continuously optimize collection scheduling algorithms and cluster architecture to support larger-scale monitoring scenarios.
|
||||
- Improve system stability, reduce resource consumption, and enhance data processing efficiency.
|
||||
- Continuously optimize collection scheduling algorithms and cluster architecture to support larger-scale monitoring scenarios.
|
||||
- Improve system stability, reduce resource consumption, and enhance data processing efficiency.
|
||||
|
||||
3. Improve Functionality and User Experience
|
||||
- Simplify the monitoring configuration process and provide a more intuitive and user-friendly interface.
|
||||
- Enhance the status page functionality and support more customization options.
|
||||
- Simplify the monitoring configuration process and provide a more intuitive and user-friendly interface.
|
||||
- Enhance the status page functionality and support more customization options.
|
||||
|
||||
4. Strengthen Community Building and Internationalization
|
||||
- Expand the community size and attract more contributors to participate in project development.
|
||||
- Improve multi-language support, including document internationalization and interface localization.
|
||||
- Expand the community size and attract more contributors to participate in project development.
|
||||
- Improve multi-language support, including document internationalization and interface localization.
|
||||
|
||||
## 4. Acknowledgments
|
||||
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
---
|
||||
title: Announcement of Apache HertzBeat™ 1.7.3 Release
|
||||
author: LiuTianyou
|
||||
author_title: LiuTianyou
|
||||
author_url: https://github.com/LiuTianyou
|
||||
tags: [opensource, release]
|
||||
keywords: [open source monitoring system, alerting system, Hertzbeat, release]
|
||||
---
|
||||
|
||||
Dear Community Members,
|
||||
|
||||
We are thrilled to announce the official release of Apache HertzBeat™ 1.7.3. This also marks the first release since Apache HertzBeat™ graduated to become a Top-Level Project at Apache.
|
||||
|
||||
## Downloads and Documentation
|
||||
|
||||
- **Apache HertzBeat™ 1.7.3 Download Link**: <https://hertzbeat.apache.org/zh-cn/docs/download>
|
||||
- **Apache HertzBeat™ Documentation**: <https://hertzbeat.apache.org/zh-cn/docs/>
|
||||
|
||||
## Major Updates
|
||||
|
||||
### New Features and Enhancements
|
||||
|
||||
- **Monitoring Support Expansion**: Added monitoring support for Apache DolphinScheduler(#3656), TDengine(#3678), macOS systems(#3715), Synology NAS(#3721), and HertzBeat(#3641). Oracle monitoring now includes user password-related metrics(#3674).
|
||||
- **Core Feature Enhancements**: Supports periodic threshold preview(#3505), export all monitoring configurations(#3509), monitoring templates support JSON nested parsing(#3645), HTTP collection supports parsing numeric data using JsonPath(#3612), and VictoriaMetrics metrics now support adding custom labels(#3622).
|
||||
- **Internationalization (i18n)**: Supports internationalization for collector online notifications(#3585).
|
||||
- **More New Features**
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Grafana Integration Issues**: Fixed visualization display issues in Grafana integration (#3666).
|
||||
- **System Stability**: Fixed startup errors when VictoriaMetrics storage is not configured (#3552), resolved premature input stream closure in Prometheus parsing (#3567), and fixed missing offline notifications when collectors go offline (#3601).
|
||||
- **Data Parsing Issues**: Fixed AES key decoding errors in collectors (#3652), resolved JDBC configuration parameter failures (#3625), and addressed multiple monitoring template parsing failures caused by Jexl keyword conflicts (#3629, #3632, #3685, #3686, #3687, #3693, #3705, #3707, #3708).
|
||||
- **Other Bug Fixes**
|
||||
|
||||
### Refactoring and Optimization
|
||||
|
||||
- **Performance Optimization**: Optimized monitoring list page loading performance (#3719), improved scheduling logic for batch refresh tasks (#3660), enabled Gzip compression for VictoriaMetrics write requests (#3595).
|
||||
- **Logic Refactoring & Improvements**: Refactored the processing logic of the Alarm Cache Manager (AlarmCacheManager) (#3525), redesigned Prometheus label parsing logic and PromQL generation logic (#3662, #3725), optimized comparators for alert judgment logic (#3574).
|
||||
- **UI/UX Improvements**: Optimized status page layout and UI (#3680), enhanced status page event list (#3727), improved homepage navigation bar layout (#3616), refined interaction effects on the new monitoring page (#3726), updated UI theme (#3682), enhanced monitoring data chart components (#3668).
|
||||
- **Other Optimizations**
|
||||
|
||||
### Tests and Quality
|
||||
|
||||
- **Unit Test Coverage**: Added unit tests for VictoriaMetrics data storage (#3595).
|
||||
- **End-to-End Testing**: Improved Kafka end-to-end (e2e) test quality (#3520).
|
||||
- **Testing Efficiency**: Implemented parallel CI to accelerate the build process (#3523).
|
||||
- **Other Testing Improvements**
|
||||
|
||||
### Documentation Enhancements
|
||||
|
||||
- **Internationalized Documentation**: Led by @Calvin979, added extensive Japanese documentation (i18n) for numerous monitoring templates, covering dozens of templates including HPE Switch, Huawei Switch, Kafka, JVM, Linux, MySQL, Redis, SpringBoot, and more.
|
||||
- **Content Corrections & Optimizations**: Fixed errors, broken links, and semantic issues in documentation (#3526, #3559, #3608, #3695).
|
||||
- **Community & Ecosystem**: Added blog posts introducing new committers and PMC members (#3522, #3561, #3563, #3569), updated project graduation-related documentation and website content (#3584, #3692, #3701, #3729), updated contributor documentation (#3667).
|
||||
- **More Documentation Updates**
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Special thanks to the following community members for their collaborative efforts:
|
||||
|
||||
> @pjfanning @chingjustwe @delei @rowankid @MasamiYui @tomsun28 @Aias00 @zhangshenghang @zqr10159 @LiuTianyou @yy549159265 @lx1229 @VampireAchao
|
||||
> @cto-huhang @pwallk @bigcyy @sarthakeash @Carpe-Wang @YxYL6125 @tuzuy @lynx009 @Duansg @carlpinto25 @Calvin979 @Cyanty @Saramanda9988
|
||||
|
||||
## What's Changed
|
||||
|
||||
```markdown
|
||||
* [release] release new version 1.7.2 by @tomsun28 in #3510
|
||||
* [doc] japanese hpe switch by @Calvin979 in #3511
|
||||
* [doc] japanese huawei switch by @Calvin979 in #3515
|
||||
* [doc] japanese hive by @Calvin979 in #3506
|
||||
* [doc] japanese hugeGraph by @Calvin979 in #3518
|
||||
* [feature] Support for Periodic Threshold preview. by @Duansg in #3505
|
||||
* [Improve] Improve kafka e2e by @zhangshenghang in #3520
|
||||
* [doc] japanese iceberg by @Calvin979 in #3519
|
||||
* [fix] Update the correct replacement of outdated functions and placeholders by @Cyanty in #3516
|
||||
* [doc] japanese influxdb by @Calvin979 in #3521
|
||||
* [doc] japanese hikvision by @Calvin979 in #3499
|
||||
* [doc] Add new commiter blog by @MasamiYui in #3522
|
||||
* [fix] fix i18n error and append two metrics about disk infos by @lx1229 in #3514
|
||||
* [feat]parallel ci by @Aias00 in #3523
|
||||
* [doc] japanese influxdb promql by @Calvin979 in #3527
|
||||
* fix: fix something by @Aias00 in #3524
|
||||
* [doc] japanese iotdb by @Calvin979 in #3530
|
||||
* download version deal by @lynx009 in #3531
|
||||
* [doc] update outdated links by @lynx009 in #3535
|
||||
* [doc]update download page text by @zqr10159 in #3532
|
||||
* [doc] japanese ipmi by @Calvin979 in #3537
|
||||
* [doc] japanese jetty by @Calvin979 in #3539
|
||||
* enable GitHub Dependabot to create PRs for security alerts but not for every dependency update by @pjfanning in #3543
|
||||
* [doc] Fix some issues in the documentation by @Duansg in #3526
|
||||
* [doc] recommend newer mysql connector jar by @pjfanning in #3541
|
||||
* [doc] add hertzbeat maturity and fix doc by @tomsun28 in #3544
|
||||
* [doc] update download page by @lynx009 in #3549
|
||||
* [doc] japanese kafka by @Calvin979 in #3548
|
||||
* [doc] japanese jvm by @Calvin979 in #3545
|
||||
* [doc] japanese kafka client by @Calvin979 in #3550
|
||||
* [feature] Support export all allmonitors by @YxYL6125 in #3509
|
||||
* feat: Add LogUtil wrapper and optimize logging comments by @Carpe-Wang in #3489
|
||||
* [refactor] AlarmCacheManager refactoring processing logic by @Duansg in #3525
|
||||
* [improve] make jackson serialize all field visibility by @tomsun28 in #3551
|
||||
* [bugfix] fix npe due the victoria insert not config when startup by @tomsun28 in #3552
|
||||
* [doc] V1.7.2 version blog by @lynx009 in #3558
|
||||
* [doc] japanese kafka promql by @Calvin979 in #3553
|
||||
* [doc] Fix documentation error and adjust Chinese semantics by @Duansg in #3559
|
||||
* [doc] add new pmc and committer by @tomsun28 in #3561
|
||||
* [doc] japanese kingbase by @Calvin979 in #3562
|
||||
* [doc] add new committer by @MasamiYui in #3563
|
||||
* [feat] Add QueryExecutor does not support internationalization. by @Duansg in #3565
|
||||
* [doc] japanese kubernetes by @Calvin979 in #3568
|
||||
* [mqtt] refact the MQTT based on the Paho SDK and support both unidirectional and bidirectional MQTT over TLS connections. by @yy549159265 in #3474
|
||||
* [docs] Add new committer blog by @bigcyy in #3569
|
||||
* [bugfix] Fix MySQL WrongArgumentException by @pwallk in #3564
|
||||
* [doc] japanese kvrocks by @Calvin979 in #3570
|
||||
* [doc] japanese linux by @Calvin979 in #3571
|
||||
* [fix]: Fix a problem where the inputstream was closed prematurely when parsing Prometheus by @zqr10159 in #3567
|
||||
* [doc]: add FAQs about task limits by @zqr10159 in #3581
|
||||
* [infra] upgrade openjdk image to fix Debian source failed by @tomsun28 in #3587
|
||||
* [feature] Support i18n of collector online notification by @Duansg in #3585
|
||||
* [doc] add japanese i18n in app-linux_script.yml by @Calvin979 in #3575
|
||||
* [doc] add japanese i18n in app-mariadb.yml by @Calvin979 in #3577
|
||||
* [doc] add japanese i18n in app-memcached.yml by @Calvin979 in #3578
|
||||
* [doc] add japanese i18n in app-modbus.yml by @Calvin979 in #3580
|
||||
* [feature] System time zone optimization. by @Duansg in #3588
|
||||
* [doc] add japanese i18n in app-mongodb.yml by @Calvin979 in #3590
|
||||
* [refactor] Refactor the comparison logic by @Duansg in #3574
|
||||
* [fix] Fix collector online metadata filling by @Duansg in #3579
|
||||
* [doc] add japanese i18n in app-mongodb_atlas.yml by @Calvin979 in #3593
|
||||
* [doc] add japanese i18n in app-mysql.yml by @cto-huhang in #3594
|
||||
* [doc] add japanese i18n in app-zookeeper.yml and app-zookeeper_sd.yml by @Cyanty in #3596
|
||||
* [doc] add japanese i18n in app-nginx.yml by @Duansg in #3600
|
||||
* [doc] add japanese i18n in app-rocketmq.yml and app-rabbitmq.yml by @Saramanda9988 in #3598
|
||||
* [improve] VM write request sets up gzip compression and adds saveData unit tests by @Cyanty in #3595
|
||||
* [feat] Auto-generate AES key if not configured by @bigcyy in #3604
|
||||
* [doc] update japanese i18n in app-mqtt.yml and app-mysql.yml by @Calvin979 in #3597
|
||||
* [fix] Fix collector offline without sending offline notification by @Duansg in #3601
|
||||
* [doc] fix image reference issues in documents by @delei in #3608
|
||||
* [doc] add japanese i18n in app-nacos.yml by @cto-huhang in #3599
|
||||
* [doc] add japanese i18n in app-nvidia.yml, app-ping.yml, app-pop3.yml, app-port.yml and app-push.yml by @Calvin979 in #3611
|
||||
* [doc] update japanese i18n in app-nacos_sd.yml, app-nebula_graph.yml, app-zookeeper.yml by @Calvin979 in #3602
|
||||
* [doc] add japanese i18n in app-nebula_graph_cluster.yml and update japanese of client by @Calvin979 in #3603
|
||||
* [bugfix] the tagValue may be null in the determineNewLabels method by @delei in #3606
|
||||
* [doc] add japanese i18n in app-netease_mailbox.yml and app-ntp.yml by @Calvin979 in #3607
|
||||
* [doc] add japanese i18n in app-oceanbase.yml by @VampireAchao in #3614
|
||||
* [feat] Http collect supports jsonpath parsing of numeric type by @Duansg in #3612
|
||||
* [doc] Optimize the home website navbar layout UI for medium-sized screens by @VampireAchao in #3616
|
||||
* [doc] add japanese i18n in app-opensuse.yml by @Calvin979 in #3619
|
||||
* [doc] add japanese i18n in app-oracle.yml by @Calvin979 in #3621
|
||||
* [doc] add japanese i18n in app-postgresql.yml by @Calvin979 in #3624
|
||||
* [doc] add linux operating system practice usecase by @delei in #3628
|
||||
* [fix] Fixed springboot3abnormal use of the keyword jexl by @Duansg in #3629
|
||||
* [Feature] add customized labels to VM metrics by @chingjustwe in #3622
|
||||
* [fix] Fixed issue where jdbc url was replaced by @Duansg in #3625
|
||||
* [fix] remove optional metrics by @Duansg in #3636
|
||||
* [doc] add japanese i18n in app-openai.yml and app-opengauss.yml by @Calvin979 in #3617
|
||||
* [doc] add japanese i18n in app-smtp.yml, app-ssl_cert.yml, app-storm.yml, app-udp_port.yml, app-uniview.yml, app-website.yml, app-websocket.yml by @Calvin979 in #3643
|
||||
* [bugfix] fix potential StringIndexOutOfBoundsException by @delei in #3642
|
||||
* [feat] Monitoring templates support JSON nested parsing. by @Duansg in #3645
|
||||
* [doc] add English version of linux operating system practice usecase by @delei in #3631
|
||||
* [bugfix] fixed jexl keyword issue in springboot2 monitoring template by @delei in #3632
|
||||
* [doc] add japanese i18n in app-qq_mailbox.yml and app-redfish.yml by @Calvin979 in #3635
|
||||
* [doc] add japanese i18n in app-prometheus.yml, app-pulsar.yml and app-registry.yml by @Calvin979 in #3639
|
||||
* [doc] add japanese i18n in app-s7.yml, app-tplink_switch.yml, app-shenyu.yml by @Calvin979 in #3648
|
||||
* [feature] Support HertzBeat self monitor by @zqr10159 in #3641
|
||||
* [fix] Fixed an issue where template configuration was overwritten by @Duansg in #3649
|
||||
* [bugfix] fix the collector aes decode with secret error by @tomsun28 in #3652
|
||||
* [doc] add japanese i18n in app-tidb.yml, app-windows_script.yml by @Calvin979 in #3650
|
||||
* [doc] add japanese i18n in app-springboot3.yml by @Duansg in #3658
|
||||
* [doc] improve documentation formatting and readability by @delei in #3659
|
||||
* [doc] add japanese i18n in app-prestodb.yml, app-spark.yml by @Calvin979 in #3651
|
||||
* [doc] add japanese i18n in app-process.yml by @Calvin979 in #3654
|
||||
* [doc] add japanese i18n in app-spring_gateway.yml, app-sqlserver.yml by @Calvin979 in #3661
|
||||
* [doc] add japanese i18n in app-redhat.yml by @Calvin979 in #3655
|
||||
* [doc] add japanese i18n and remove chinese in script folder by @Calvin979 in #3665
|
||||
* [doc] update contribution doc by @tomsun28 in #3667
|
||||
* [GSOC] MCP server setup, authorization, and basic tool support by @sarthakeash in #3610
|
||||
* [doc] add japanese i18n in app-redis.yml by @Calvin979 in #3669
|
||||
* [Feature] add Apache DolphinScheduler monitoring support by @delei in #3656
|
||||
* [improve] Optimize the scheduling logic for batch flush tasks by @Cyanty in #3660
|
||||
* [fix] Fixed Grafana visualization integration display issue by @Duansg in #3666
|
||||
* [feature] add user password monitoring metrics in oracle monitor by @delei in #3674
|
||||
* [doc] add japanese i18n in app-redis_cluster.yml by @Calvin979 in #3672
|
||||
* [doc] add japanese i18n in app-dolphinscheduler.yml by @Calvin979 in #3677
|
||||
* [improve] optimize status page layout and UI by @delei in #3680
|
||||
* [doc] add japanese i18n in app-redis_sentinel.yml by @Calvin979 in #3681
|
||||
* [doc] remove all (incubating) by @VampireAchao in #3584
|
||||
* [bugfix] fixed jexl keyword issue in postgresql monitoring template by @delei in #3685
|
||||
* [bugfix] fixed jexl keyword issue in hertzbeat monitoring template by @delei in #3686
|
||||
* [bugfix] fixed jexl keyword issue in windows monitoring template by @delei in #3687
|
||||
* [bugfix] fixed jexl keyword issue in spring_gateway monitoring template by @delei in #3693
|
||||
* [doc] update doc while graduate by @tomsun28 in #3692
|
||||
* [doc] Fixed menu path errors and MD document format anomalies. by @Duansg in #3695
|
||||
* chore: update ui theme by @tomsun28 in #3682
|
||||
* [doc] add japanese i18n in app-rockylinux.yml by @Calvin979 in #3688
|
||||
* [doc] add japanese i18n in app-seatunnel.yml by @Calvin979 in #3691
|
||||
* [doc] add japanese i18n in app-tomcat.yml by @Calvin979 in #3700
|
||||
* [refactor] modify message column to TEXT type by @delei in #3698
|
||||
* [doc] update website, doc while graduate by @tomsun28 in #3701
|
||||
* [bugfix] resolve translation value fetching for uppercase app names by @rowankid in #3690
|
||||
* [doc] add japanese i18n in app-vastbase.yml by @Calvin979 in #3704
|
||||
* [bugfix] fixed jexl keyword issue in kingbase monitoring template by @delei in #3705
|
||||
* [feat] Support TDengine monitoring by @Duansg in #3678
|
||||
* [bugfix] fixed jexl keyword issue in greenplum monitoring template by @delei in #3707
|
||||
* [bugfix] fixed jexl keyword issue in vastbase monitoring template by @delei in #3708
|
||||
* refactor: Optimize the monitoring data chart component by @tuzuy in #3668
|
||||
* [improve] Improve the parsing logic of prometheus label by @Duansg in #3662
|
||||
* [doc] modify github link. by @lynx009 in #3714
|
||||
* [doc] add japanese i18n in app-ubuntu.yml, app-windows.yml and app-yarn.yml by @Calvin979 in #3703
|
||||
* [doc] add japanese i18n in app-valkey.yml by @Calvin979 in #3706
|
||||
* [improve] Improve performance issues when loading large numbers of metric cards in Monitors by @Duansg in #3719
|
||||
* [doc] add japanese i18n in app-tdengine.yml by @Calvin979 in #3717
|
||||
* [feature] add macOS monitoring support by @delei in #3715
|
||||
* [feature] add Synology NAS monitoring support by @delei in #3721
|
||||
* [improve] Improve Prometheus label PromQL parsing logic by @Duansg in #3725
|
||||
* [release] release new version 1.7.3 by @LiuTianyou in #3724
|
||||
* [Improve] changed scroll of intervals in New Monitor page by @carlpinto25 in #3726
|
||||
* [improve] optimize the incident list on the status page to support paginated queries by @delei in #3727
|
||||
* docs: update doc and add graduation blog by @tomsun28 in #3729
|
||||
```
|
||||
|
||||
## New Contributors
|
||||
|
||||
- @yy549159265 made their first contribution in <https://github.com/apache/hertzbeat/pull/3474>
|
||||
- @cto-huhang made their first contribution in <https://github.com/apache/hertzbeat/pull/3594>
|
||||
- @Saramanda9988 made their first contribution in <https://github.com/apache/hertzbeat/pull/3598>
|
||||
- @delei made their first contribution in <https://github.com/apache/hertzbeat/pull/3608>
|
||||
- @chingjustwe made their first contribution in <https://github.com/apache/hertzbeat/pull/3622>
|
||||
- @rowankid made their first contribution in <https://github.com/apache/hertzbeat/pull/3690>
|
||||
- @tuzuy made their first contribution in <https://github.com/apache/hertzbeat/pull/3668>
|
||||
- @carlpinto25 made their first contribution in <https://github.com/apache/hertzbeat/pull/3726>
|
||||
|
||||
## Apache Hertzbeat
|
||||
|
||||
**Repository URL:**
|
||||
|
||||
<https://github.com/apache/hertzbeat>
|
||||
|
||||
**Official Website:**
|
||||
|
||||
<https://hertzbeat.apache.org/>
|
||||
|
||||
**Apache HertzBeat™ Download Link:**
|
||||
|
||||
<https://hertzbeat.apache.org/docs/download>
|
||||
|
||||
**Apache HertzBeat™ Docker Images:**
|
||||
|
||||
Apache HertzBeat™ provides Docker images for each release, available on Docker Hub:
|
||||
|
||||
- HertzBeat: <https://hub.docker.com/r/apache/hertzbeat>
|
||||
- HertzBeat Collector: <https://hub.docker.com/r/apache/hertzbeat-collector>
|
||||
|
||||
**How to Contribute to the Apache HertzBeat™ Open Source Community?**
|
||||
|
||||
<https://hertzbeat.apache.org/docs/community/contribution>
|
||||
@@ -143,7 +143,7 @@ git pull upstream master
|
||||
|
||||
After your pr is merged, you can send an email `dev@hertzbeat.apache.org` to request your Apache HertzBeat™ contributor electronic certificate.
|
||||
|
||||
```
|
||||
```text
|
||||
Title: [Contributor] Request for a Contributor Certificate: <Your Full Name>
|
||||
Body:
|
||||
Hello,
|
||||
@@ -165,7 +165,6 @@ Certificate Sample:
|
||||
|
||||

|
||||
|
||||
|
||||
### HertzBeat Improvement Proposal (HIP)
|
||||
|
||||
If you have major new features(e.g., support metrics push gateway, support logs monitoring), you need to write a design document known as a HertzBeat Improvement Proposal (HIP). Before starting to write a HIP, make sure you follow the process [guide](https://github.com/apache/hertzbeat/tree/master/hip).
|
||||
@@ -176,5 +175,4 @@ With the above steps, you are a contributor to HertzBeat. Repeat the previous st
|
||||
|
||||
### Join Discussion
|
||||
|
||||
[Discussion](contact)
|
||||
|
||||
[Discussion](contact)
|
||||
|
||||
@@ -4,7 +4,7 @@ title: How to Release
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
This tutorial describes in detail how to release Apache HertzBeat™, take the release of version 1.6.0 as an example.
|
||||
This tutorial describes in detail how to release Apache HertzBeat™, take the release of version 1.7.3 as an example.
|
||||
|
||||
## 1. Environmental requirements
|
||||
|
||||
@@ -133,8 +133,8 @@ Or enter <https://keyserver.ubuntu.com/> address in the browser, enter the name
|
||||
|
||||
#### 2.4 Add the gpg public key to the KEYS file of the Apache SVN project repo
|
||||
|
||||
- Apache HertzBeat™ Branch Dev <https://dist.apache.org/repos/dist/dev/incubator/hertzbeat>
|
||||
- Apache HertzBeat™ Branch Release <https://dist.apache.org/repos/dist/release/incubator/hertzbeat>
|
||||
- Apache HertzBeat™ Branch Dev <https://dist.apache.org/repos/dist/dev/hertzbeat>
|
||||
- Apache HertzBeat™ Branch Release <https://dist.apache.org/repos/dist/release/hertzbeat>
|
||||
|
||||
##### 2.4.1 Add public key to KEYS in dev branch
|
||||
|
||||
@@ -142,7 +142,7 @@ Or enter <https://keyserver.ubuntu.com/> address in the browser, enter the name
|
||||
$ mkdir -p svn/dev
|
||||
$ cd svn/dev
|
||||
|
||||
$ svn co https://dist.apache.org/repos/dist/dev/incubator/hertzbeat
|
||||
$ svn co https://dist.apache.org/repos/dist/dev/hertzbeat
|
||||
$ cd svn/dev/hertzbeat
|
||||
|
||||
# Append the KEY you generated to the file KEYS, and check if it is added correctly
|
||||
@@ -157,7 +157,7 @@ $ svn ci -m "add gpg key for muchunjin"
|
||||
$ mkdir -p svn/release
|
||||
$ cd svn/release
|
||||
|
||||
$ svn co https://dist.apache.org/repos/dist/release/incubator/hertzbeat
|
||||
$ svn co https://dist.apache.org/repos/dist/release/hertzbeat
|
||||
$ cd svn/release/hertzbeat
|
||||
|
||||
# Append the KEY you generated to the file KEYS, and check if it is added correctly
|
||||
@@ -170,17 +170,17 @@ $ svn ci -m "add gpg key for muchunjin"
|
||||
|
||||
### Build Package
|
||||
|
||||
#### 3.1 Based on the master branch, create a release-${release_version}-rcx branch, such as release-1.6.0-rc1, And create a tag named v1.6.0-rc1 based on the release-1.6.0-rc1 branch, and set this tag as pre-release
|
||||
#### 3.1 Based on the master branch, create a release-${release_version}-rcx branch, such as release-1.7.3-rc1, And create a tag named v1.7.3-rc1 based on the release-1.7.3-rc1 branch, and set this tag as pre-release
|
||||
|
||||
```shell
|
||||
git checkout master
|
||||
git checkout -b release-1.6.0-rc1
|
||||
git checkout -b release-1.7.3-rc1
|
||||
```
|
||||
|
||||
#### 3.2 checkout release branch to local
|
||||
|
||||
```shell
|
||||
git checkout release-1.6.0-rc1
|
||||
git checkout release-1.7.3-rc1
|
||||
```
|
||||
|
||||
#### 3.3 Compile the binary package
|
||||
@@ -224,12 +224,12 @@ The release package are here:
|
||||
```shell
|
||||
git archive \
|
||||
--format=tar.gz \
|
||||
--output="dist/apache-hertzbeat-1.6.0-src.tar.gz" \
|
||||
--prefix=apache-hertzbeat-1.6.0-src/ \
|
||||
release-1.6.0-rc1
|
||||
--output="dist/apache-hertzbeat-1.7.3-src.tar.gz" \
|
||||
--prefix=apache-hertzbeat-1.7.3-src/ \
|
||||
release-1.7.3-rc1
|
||||
```
|
||||
|
||||
The archive package is here `dist/apache-hertzbeat-1.6.0-src.tar.gz`
|
||||
The archive package is here `dist/apache-hertzbeat-1.7.3-src.tar.gz`
|
||||
|
||||
### Sign package
|
||||
|
||||
@@ -252,18 +252,18 @@ for i in *.tar.gz; do echo $i; sha512sum $i > $i.sha512 ; done
|
||||
> The final file list is as follows
|
||||
|
||||
```text
|
||||
apache-hertzbeat-1.6.0-src.tar.gz
|
||||
apache-hertzbeat-1.6.0-src.tar.gz.asc
|
||||
apache-hertzbeat-1.6.0-src.tar.gz.sha512
|
||||
apache-hertzbeat-1.6.0-bin.tar.gz
|
||||
apache-hertzbeat-1.6.0-bin.tar.gz.asc
|
||||
apache-hertzbeat-1.6.0-bin.tar.gz.sha512
|
||||
apache-hertzbeat-1.6.0-docker-compose.tar.gz
|
||||
apache-hertzbeat-1.6.0-docker-compose.tar.gz.asc
|
||||
apache-hertzbeat-1.6.0-docker-compose.tar.gz.sha512
|
||||
apache-hertzbeat-collector-1.6.0-bin.tar.gz
|
||||
apache-hertzbeat-collector-1.6.0-bin.tar.gz.asc
|
||||
apache-hertzbeat-collector-1.6.0-bin.tar.gz.sha512
|
||||
apache-hertzbeat-1.7.3-src.tar.gz
|
||||
apache-hertzbeat-1.7.3-src.tar.gz.asc
|
||||
apache-hertzbeat-1.7.3-src.tar.gz.sha512
|
||||
apache-hertzbeat-1.7.3-bin.tar.gz
|
||||
apache-hertzbeat-1.7.3-bin.tar.gz.asc
|
||||
apache-hertzbeat-1.7.3-bin.tar.gz.sha512
|
||||
apache-hertzbeat-1.7.3-docker-compose.tar.gz
|
||||
apache-hertzbeat-1.7.3-docker-compose.tar.gz.asc
|
||||
apache-hertzbeat-1.7.3-docker-compose.tar.gz.sha512
|
||||
apache-hertzbeat-collector-1.7.3-bin.tar.gz
|
||||
apache-hertzbeat-collector-1.7.3-bin.tar.gz.asc
|
||||
apache-hertzbeat-collector-1.7.3-bin.tar.gz.sha512
|
||||
```
|
||||
|
||||
#### 3.6 Verify signature
|
||||
@@ -274,30 +274,34 @@ $ cd dist
|
||||
# Verify signature
|
||||
$ for i in *.tar.gz; do echo $i; gpg --verify $i.asc $i ; done
|
||||
|
||||
apache-hertzbeat-1.6.0-src.tar.gz
|
||||
apache-hertzbeat-1.7.3-bin.tar.gz
|
||||
gpg: Signature made Tue May 2 12:16:35 2023 CST
|
||||
gpg: using RSA key 85778A4CE4DD04B7E07813ABACFB69E705016886
|
||||
gpg: Good signature from "muchunjin (apache key) <muchunjin@apache.org>" [ultimate]
|
||||
apache-hertzbeat_2.11-1.6.0-bin.tar.gz
|
||||
apache-hertzbeat-1.7.3-docker-compose.tar.gz
|
||||
gpg: Signature made Tue May 2 12:16:36 2023 CST
|
||||
gpg: using RSA key 85778A4CE4DD04B7E07813ABACFB69E705016886
|
||||
gpg: Good signature from "muchunjin (apache key) <muchunjin@apache.org>" [ultimate]
|
||||
apache-hertzbeat_2.12-1.6.0-bin.tar.gz
|
||||
apache-hertzbeat-1.7.3-src.tar.gz
|
||||
gpg: Signature made Tue May 2 12:16:37 2023 CST
|
||||
gpg: using RSA key 85778A4CE4DD04B7E07813ABACFB69E705016886
|
||||
gpg: BAD signature from "muchunjin (apache key) <muchunjin@apache.org>" [ultimate]
|
||||
gpg: Good signature from "muchunjin (apache key) <muchunjin@apache.org>" [ultimate]
|
||||
apache-hertzbeat-collector-1.7.3-bin.tar.gz
|
||||
gpg: Signature made Tue May 2 12:16:37 2023 CST
|
||||
gpg: using RSA key 85778A4CE4DD04B7E07813ABACFB69E705016886
|
||||
gpg: Good signature from "muchunjin (apache key) <muchunjin@apache.org>" [ultimate]
|
||||
|
||||
# Verify SHA512
|
||||
$ for i in *.tar.gz; do echo $i; sha512sum --check $i.sha512; done
|
||||
|
||||
apache-hertzbeat-1.6.0-src.tar.gz
|
||||
apache-hertzbeat-1.6.0-src.tar.gz: OK
|
||||
apache-hertzbeat-1.6.0-bin.tar.gz
|
||||
apache-hertzbeat-1.6.0-bin.tar.gz: OK
|
||||
apache-hertzbeat-1.6.0-docker-compose.tar.gz
|
||||
apache-hertzbeat-1.6.0-docker-compose.tar.gz: OK
|
||||
apache-hertzbeat-collector-1.6.0-bin.tar.gz
|
||||
apache-hertzbeat-collector-1.6.0-bin.tar.gz: OK
|
||||
apache-hertzbeat-1.7.3-src.tar.gz
|
||||
apache-hertzbeat-1.7.3-src.tar.gz: OK
|
||||
apache-hertzbeat-1.7.3-bin.tar.gz
|
||||
apache-hertzbeat-1.7.3-bin.tar.gz: OK
|
||||
apache-hertzbeat-1.7.3-docker-compose.tar.gz
|
||||
apache-hertzbeat-1.7.3-docker-compose.tar.gz: OK
|
||||
apache-hertzbeat-collector-1.7.3-bin.tar.gz
|
||||
apache-hertzbeat-collector-1.7.3-bin.tar.gz: OK
|
||||
```
|
||||
|
||||
#### 3.7 Publish the dev directory of the Apache SVN material package
|
||||
@@ -314,12 +318,12 @@ svn co --depth empty https://dist.apache.org/repos/dist/dev/incubator/hertzbeat
|
||||
- Copy the material package to the dev directory
|
||||
|
||||
Create a version number directory and name it in the form of ${release_version}-${RC_version}. RC_version starts from 1, that is, the candidate version starts from RC1. During the release process, there is a problem that causes the vote to fail.
|
||||
If it needs to be corrected, it needs to iterate the RC version , the RC version number needs to be +1. For example: Vote for version 1.6.0-RC1. If the vote passes without any problems, the RC1 version material will be released as the final version material.
|
||||
If there is a problem (when the hertzbeat/incubator community votes, the voters will strictly check various release requirements and compliance issues) and need to be corrected, then re-initiate the vote after the correction, and the candidate version for the next vote is 1.6.0- RC2.
|
||||
If it needs to be corrected, it needs to iterate the RC version , the RC version number needs to be +1. For example: Vote for version 1.7.3-RC1. If the vote passes without any problems, the RC1 version material will be released as the final version material.
|
||||
If there is a problem (when the hertzbeat/incubator community votes, the voters will strictly check various release requirements and compliance issues) and need to be corrected, then re-initiate the vote after the correction, and the candidate version for the next vote is 1.7.3- RC2.
|
||||
|
||||
```shell
|
||||
mkdir -p svn/dev/1.6.0-RC1
|
||||
cp -f dist/* svn/dev/1.6.0-RC1
|
||||
mkdir -p svn/dev/1.7.3-RC1
|
||||
cp -f dist/* svn/dev/1.7.3-RC1
|
||||
```
|
||||
|
||||
- Commit to SVN
|
||||
@@ -331,17 +335,17 @@ cd svn/dev
|
||||
svn status
|
||||
|
||||
# 2. add to svn
|
||||
svn add 1.6.0-RC1
|
||||
svn add 1.7.3-RC1
|
||||
|
||||
svn status
|
||||
|
||||
# 3. Submit to svn remote server
|
||||
svn commit -m "release for HertzBeat 1.6.0"
|
||||
svn commit -m "release for HertzBeat 1.7.3"
|
||||
```
|
||||
|
||||
- Check Apache SVN Commit Results
|
||||
|
||||
> Visit the address <https://dist.apache.org/repos/dist/dev/incubator/hertzbeat/> in the browser, check if existed the new material package
|
||||
> Visit the address <https://dist.apache.org/repos/dist/dev/hertzbeat/> in the browser, check if existed the new material package
|
||||
|
||||
## 4. Enter the community voting stage
|
||||
|
||||
@@ -350,24 +354,24 @@ svn commit -m "release for HertzBeat 1.6.0"
|
||||
Send a voting email in the community requires at least three `+1` and no `-1`.
|
||||
|
||||
> `Send to`: <dev@hertzbeat.apache.org> <br />
|
||||
> `Title`: [VOTE] Release Apache HertzBeat™ 1.6.0 rc1 <br />
|
||||
> `Title`: [VOTE] Release Apache HertzBeat™ 1.7.3 rc1 <br />
|
||||
> `Body`:
|
||||
|
||||
```text
|
||||
Hello HertzBeat Community:
|
||||
|
||||
This is a call for vote to release Apache HertzBeat™ version release-1.6.0-RC1.
|
||||
This is a call for vote to release Apache HertzBeat™ version release-1.7.3-RC1.
|
||||
|
||||
Apache HertzBeat™ - A real-time observability system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
|
||||
|
||||
Release notes:
|
||||
https://github.com/apache/hertzbeat/releases/tag/v1.6.0-rc1
|
||||
https://github.com/apache/hertzbeat/releases/tag/v1.7.3-rc1
|
||||
|
||||
The release candidates:
|
||||
https://dist.apache.org/repos/dist/dev/incubator/hertzbeat/1.6.0-RC1/
|
||||
https://dist.apache.org/repos/dist/dev/incubator/hertzbeat/1.7.3-RC1/
|
||||
|
||||
Git tag for the release:
|
||||
https://github.com/apache/hertzbeat/releases/tag/v1.6.0-rc1
|
||||
https://github.com/apache/hertzbeat/releases/tag/v1.7.3-rc1
|
||||
|
||||
The artifacts signed with PGP key [33545C76], corresponding to [muchunjin@apache.org], that can be found in keys file:
|
||||
https://downloads.apache.org/incubator/hertzbeat/KEYS
|
||||
@@ -391,9 +395,6 @@ release.
|
||||
[ ] No compiled archives bundled in source archive.
|
||||
[ ] Can compile from source.
|
||||
|
||||
More detail checklist please refer:
|
||||
https://cwiki.apache.org/confluence/display/INCUBATOR/Incubator+Release+Checklist
|
||||
|
||||
Steps to validate the release,Please refer to:
|
||||
https://hertzbeat.apache.org/docs/community/how_to_verify_release
|
||||
|
||||
@@ -406,13 +407,13 @@ Thanks!
|
||||
After 72 hours, the voting results will be counted, and the voting result email will be sent, as follows.
|
||||
|
||||
> `Send to`: <dev@hertzbeat.apache.org> <br />
|
||||
> `Title`: [RESULT]\[VOTE\] Release Apache HertzBeat™ 1.6.0-rc1 <br />
|
||||
> `Title`: [RESULT]\[VOTE\] Release Apache HertzBeat™ 1.7.3-rc1 <br />
|
||||
> `Body`:
|
||||
|
||||
```text
|
||||
Dear HertzBeat community,
|
||||
|
||||
Thanks for your review and vote for "Release Apache HertzBeat™ 1.6.0-rc1"
|
||||
Thanks for your review and vote for "Release Apache HertzBeat™ 1.7.3-rc1"
|
||||
I'm happy to announce the vote has passed:
|
||||
---
|
||||
4 binding +1, from:
|
||||
@@ -436,112 +437,12 @@ ChunJin Mu
|
||||
|
||||
One item of the email content is `Vote thread`, and the link is obtained here: <https://lists.apache.org/list.html?dev@hertzbeat.apache.org>
|
||||
|
||||
#### 3.2 Send Incubator Community voting mail
|
||||
## 4. Complete the final publishing steps
|
||||
|
||||
Send a voting email in the incubator community requires at least three `+1` and no `-1`.
|
||||
|
||||
> `Send to`: <general@incubator.apache.org> <br />
|
||||
> `Title`: [VOTE] Release Apache HertzBeat™ 1.6.0-rc1 <br />
|
||||
> `Body`:
|
||||
|
||||
```text
|
||||
Hello Incubator Community:
|
||||
|
||||
This is a call for a vote to release Apache HertzBeat™ version 1.6.0-RC1.
|
||||
The Apache HertzBeat™ community has voted on and approved a proposal to release Apache HertzBeat™ version 1.6.0-RC1.
|
||||
We now kindly request the Incubator PPMC members review and vote on this incubator release.
|
||||
Apache HertzBeat™, A real-time observability system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
|
||||
|
||||
HertzBeat community vote thread:
|
||||
https://lists.apache.org/thread/t01b2lbtqzyt7j4dsbdp5qjc3gngjsdq
|
||||
|
||||
Vote result thread:
|
||||
https://lists.apache.org/thread/t5z58mvrs1drgzfyc48c9lhmd8skswn7
|
||||
|
||||
The release candidate:
|
||||
https://dist.apache.org/repos/dist/dev/incubator/hertzbeat/1.6.0-RC1/
|
||||
|
||||
Git tag for the release:
|
||||
https://github.com/apache/hertzbeat/releases/tag/v1.6.0-rc1
|
||||
|
||||
The artifacts signed with PGP key [33545C76], corresponding to [muchunjin@apache.org], that can be found in keys file:
|
||||
https://downloads.apache.org/incubator/hertzbeat/KEYS
|
||||
|
||||
The vote will be open for at least 72 hours or until the necessary number of votes are reached.
|
||||
|
||||
Please vote accordingly:
|
||||
[ ] +1 approve
|
||||
[ ] +0 no opinion
|
||||
[ ] -1 disapprove with the reason
|
||||
|
||||
More detailed checklist please refer:
|
||||
• https://cwiki.apache.org/confluence/display/INCUBATOR/Incubator+Release+Checklist
|
||||
|
||||
Steps to validate the release, Please refer to:
|
||||
• https://www.apache.org/info/verification.html
|
||||
• https://hertzbeat.apache.org/docs/community/how_to_verify_release
|
||||
---
|
||||
How to Build:
|
||||
https://hertzbeat.apache.org/docs/community/development/#build-hertzbeat-binary-package
|
||||
---
|
||||
Thanks,
|
||||
|
||||
On behalf of Apache HertzBeat™ community
|
||||
---
|
||||
Best,
|
||||
ChunJin Mu
|
||||
```
|
||||
|
||||
If there is no -1 after 72 hours, reply to the email as follows
|
||||
|
||||
> `Send to`: <general@incubator.apache.org> <br />
|
||||
> `Body`:
|
||||
|
||||
```text
|
||||
Thanks everyone for review and vote, 72H passed. I'll announce the vote result soon.
|
||||
|
||||
Best,
|
||||
Chunjin Mu
|
||||
```
|
||||
|
||||
Then the voting results will be counted, and the voting result email will be sent, as follows.
|
||||
|
||||
> `Send to`: <general@incubator.apache.org> <br />
|
||||
> `Title`: [RESULT]\[VOTE\] Release Apache HertzBeat™ 1.6.0-rc1 <br />
|
||||
> `Body`:
|
||||
|
||||
```text
|
||||
Hi Incubator Community,
|
||||
|
||||
The vote to release Apache HertzBeat™ 1.6.0-rc4 has passed with 3 +1 binding and no +0 or -1 votes.
|
||||
|
||||
3 binding votes, no +0 or -1 votes.
|
||||
|
||||
+3 (binding) +1, from:
|
||||
- xxx
|
||||
|
||||
no 0 or -1 votes.
|
||||
|
||||
Vote thread:
|
||||
|
||||
https://lists.apache.org/thread/m1kyn4l30y55p6q39m0ys5djvdd73h0f
|
||||
|
||||
Thanks everyone for your feedback and help with HertzBeat apache release. The HertzBeat team will take the steps to complete this release and will announce it soon.
|
||||
|
||||
Best,
|
||||
ChunJin Mu
|
||||
```
|
||||
|
||||
One item of the email content is `Vote thread`, and the link is obtained here: <https://lists.apache.org/list.html?general@incubator.apache.org>
|
||||
|
||||
Wait a day to see if the tutor has any other comments, if not, send the following announcement email
|
||||
|
||||
## 5. Complete the final publishing steps
|
||||
|
||||
### 5.1 Migrating source and binary packages
|
||||
### 4.1 Migrating source and binary packages
|
||||
|
||||
```shell
|
||||
svn mv https://dist.apache.org/repos/dist/dev/incubator/hertzbeat/1.6.0-RC1 https://dist.apache.org/repos/dist/release/incubator/hertzbeat/1.6.0 -m "transfer packages for 1.6.0-RC1"
|
||||
svn mv https://dist.apache.org/repos/dist/dev/hertzbeat/1.7.3-RC1 https://dist.apache.org/repos/dist/release/hertzbeat/1.7.3 -m "transfer packages for 1.7.3-RC1"
|
||||
```
|
||||
|
||||
#### 4.2 Add the new version download address to the official website
|
||||
@@ -555,7 +456,7 @@ Open the official website address <https://hertzbeat.apache.org/docs/download/>
|
||||
|
||||
#### 4.3 Generate a release on github
|
||||
|
||||
Update pre-release to create a tag named v1.6.0 based on the release-1.6.0-rc1 branch, and set this tag to latest release.
|
||||
Update pre-release to create a tag named v1.7.3 based on the release-1.7.3-rc1 branch, and set this tag to latest release.
|
||||
|
||||
:::tip
|
||||
You can modify it on the original RC Release without creating a new Release.
|
||||
@@ -566,7 +467,7 @@ Then enter Release Title and Describe
|
||||
- Release Title:
|
||||
|
||||
```text
|
||||
v1.6.0
|
||||
v1.7.3
|
||||
```
|
||||
|
||||
- Describe:
|
||||
@@ -578,19 +479,19 @@ release note: xxx
|
||||
|
||||
Then click the `Publish release` button.
|
||||
|
||||
The rename the release-1.6.0-rc1 branch to release-1.6.0.
|
||||
The rename the release-1.7.3-rc1 branch to release-1.7.3.
|
||||
|
||||
#### 4.5 Send new version announcement email
|
||||
|
||||
> `Send to`: <general@incubator.apache.org> <br />
|
||||
> `Send to`: <announce@apache.org> <br />
|
||||
> `cc`: <dev@hertzbeat.apache.org> <br />
|
||||
> `Title`: [ANNOUNCE] Apache HertzBeat™ 1.6.0 released <br />
|
||||
> `Title`: [ANNOUNCE] Apache HertzBeat™ 1.7.3 released <br />
|
||||
> `Body`:
|
||||
|
||||
```text
|
||||
Hi Community,
|
||||
|
||||
We are glad to announce the release of Apache HertzBeat™ 1.6.0.
|
||||
We are glad to announce the release of Apache HertzBeat™ 1.7.3.
|
||||
Thanks again for your help.
|
||||
|
||||
Apache HertzBeat™ (https://hertzbeat.apache.org/) - A real-time observability system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
|
||||
@@ -599,7 +500,7 @@ Download Link:
|
||||
https://hertzbeat.apache.org/docs/download/
|
||||
|
||||
Release Note:
|
||||
https://github.com/apache/hertzbeat/releases/tag/v1.6.0
|
||||
https://github.com/apache/hertzbeat/releases/tag/v1.7.3
|
||||
|
||||
Website:
|
||||
https://hertzbeat.apache.org/
|
||||
|
||||
@@ -9,7 +9,7 @@ sidebar_label: Download
|
||||
|
||||
:::tip
|
||||
|
||||
- Please verify the release with corresponding hashes(sha512), signatures and [project release KEYS](https://downloads.apache.org/incubator/hertzbeat/KEYS).
|
||||
- Please verify the release with corresponding hashes(sha512), signatures and [project release KEYS](https://downloads.apache.org/hertzbeat/KEYS).
|
||||
- Refer to [How to Verify](https://www.apache.org/dyn/closer.cgi#verify) for how to check the hashes and signatures.
|
||||
|
||||
:::
|
||||
@@ -20,9 +20,9 @@ sidebar_label: Download
|
||||
Previous releases of HertzBeat may be affected by security issues, please use the latest one.
|
||||
:::
|
||||
|
||||
| Version | Date | Download | Release |
|
||||
|---------|------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------|
|
||||
| v1.7.2 | 2025.07.05 | [apache-hertzbeat-1.7.2-incubating-bin.tar.gz](https://www.apache.org/dyn/closer.lua/incubator/hertzbeat/1.7.2/apache-hertzbeat-1.7.2-incubating-bin.tar.gz) (Server) ( [signature](https://downloads.apache.org/incubator/hertzbeat/1.7.2/apache-hertzbeat-1.7.2-incubating-bin.tar.gz.asc) , [sha512](https://downloads.apache.org/incubator/hertzbeat/1.7.2/apache-hertzbeat-1.7.2-incubating-bin.tar.gz.sha512) ) <br/> [apache-hertzbeat-collector-1.7.2-incubating-bin.tar.gz](https://www.apache.org/dyn/closer.lua/incubator/hertzbeat/1.7.2/apache-hertzbeat-collector-1.7.2-incubating-bin.tar.gz) (Collector) ( [signature](https://downloads.apache.org/incubator/hertzbeat/1.7.2/apache-hertzbeat-collector-1.7.2-incubating-bin.tar.gz.asc) , [sha512](https://downloads.apache.org/incubator/hertzbeat/1.7.2/apache-hertzbeat-collector-1.7.2-incubating-bin.tar.gz.sha512) ) <br/> [apache-hertzbeat-1.7.2-incubating-src.tar.gz](https://www.apache.org/dyn/closer.lua/incubator/hertzbeat/1.7.2/apache-hertzbeat-1.7.2-incubating-src.tar.gz) (Source Code) ( [signature](https://downloads.apache.org/incubator/hertzbeat/1.7.2/apache-hertzbeat-1.7.2-incubating-src.tar.gz.asc) , [sha512](https://downloads.apache.org/incubator/hertzbeat/1.7.2/apache-hertzbeat-1.7.2-incubating-src.tar.gz.sha512) ) <br/> [apache-hertzbeat-1.7.2-incubating-docker-compose.tar.gz](https://www.apache.org/dyn/closer.lua/incubator/hertzbeat/1.7.2/apache-hertzbeat-1.7.2-incubating-docker-compose.tar.gz) (Docker Compose) ( [signature](https://downloads.apache.org/incubator/hertzbeat/1.7.2/apache-hertzbeat-1.7.2-incubating-docker-compose.tar.gz.asc) , [sha512](https://downloads.apache.org/incubator/hertzbeat/1.7.2/apache-hertzbeat-1.7.2-incubating-docker-compose.tar.gz.sha512) ) | [note](https://github.com/apache/hertzbeat/releases/tag/v1.7.2) |
|
||||
| Version | Date | Download | Release |
|
||||
|---------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------|
|
||||
| v1.7.3 | 2025.09.05 | [apache-hertzbeat-1.7.3-bin.tar.gz](https://www.apache.org/dyn/closer.lua/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-bin.tar.gz) (Server) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-bin.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-bin.tar.gz.sha512) ) <br/> [apache-hertzbeat-collector-1.7.3-bin.tar.gz](https://www.apache.org/dyn/closer.lua/hertzbeat/1.7.3/apache-hertzbeat-collector-1.7.3-bin.tar.gz) (Collector) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-collector-1.7.3-bin.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-collector-1.7.3-bin.tar.gz.sha512) ) <br/> [apache-hertzbeat-1.7.3-src.tar.gz](https://www.apache.org/dyn/closer.lua/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-src.tar.gz) (Source Code) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-src.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-src.tar.gz.sha512) ) <br/> [apache-hertzbeat-1.7.3-docker-compose.tar.gz](https://github.com/apache/hertzbeat/releases/download/v1.7.3/apache-hertzbeat-1.7.3-docker-compose.tar.gz) (Docker Compose) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-docker-compose.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-docker-compose.tar.gz.sha512) ) | [note](https://github.com/apache/hertzbeat/releases/tag/v1.7.3) |
|
||||
|
||||
## Release Docker Image
|
||||
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
---
|
||||
id: ai_agent_chat
|
||||
title: AI Agent Chat User Guide
|
||||
sidebar_label: AI Agent Chat
|
||||
keywords: [AI, Chat, Agent, Monitoring, AI Agent, OpenAI]
|
||||
---
|
||||
|
||||
> HertzBeat AI Agent Chat is an intelligent monitoring AI Agent that helps you manage monitors, configure alerts, and optimize your infrastructure monitoring through natural language conversation.
|
||||
|
||||
## Overview
|
||||
|
||||
The AI Agent Chat feature provides an interactive chat interface where you can:
|
||||
|
||||
**Monitor Management:**
|
||||
|
||||
- 🔍 Query and filter existing monitors by status, type, host, and labels
|
||||
- ➕ Add new monitors for websites, APIs, databases, and services
|
||||
- 📊 Get detailed information about available monitor types and their parameters
|
||||
- ⚡ Check monitor status and troubleshoot monitoring issues
|
||||
|
||||
**Alert Management:**
|
||||
|
||||
- 🚨 Query active alerts with comprehensive filtering (type, status, search)
|
||||
- 📈 Get alert summary statistics and distribution
|
||||
- 🔔 View both single and grouped alerts
|
||||
- 📋 Analyze alert patterns and trends
|
||||
|
||||
**Metrics Analysis:**
|
||||
|
||||
- 📊 Retrieve real-time metrics data for any monitor
|
||||
- 📈 Access historical metrics with customizable time ranges
|
||||
- 💾 Check warehouse storage system status
|
||||
- 🔍 Query specific metric fields and labels
|
||||
|
||||
**Alert Rule Management:**
|
||||
|
||||
- ⚙️ Configure alert rules and thresholds
|
||||
- 📝 Manage alert definitions for different monitor types
|
||||
- 🎯 Set up custom alerting criteria
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before using the AI Agent Chat, ensure:
|
||||
|
||||
1 **OpenAI Configuration**: Valid OpenAI API key must be configured. Optionally, you can change the model in `application.yml` under `spring.ai.openai.chat.options.model` (default is `gpt-4.1-nano`).
|
||||
2 **Database Connection**: HertzBeat database must be accessible for monitor operations
|
||||
|
||||
## Configuration
|
||||
|
||||
### OpenAI API Key Setup
|
||||
|
||||
The AI Agent Chat uses OpenAI's GPT models. You need to configure an OpenAI API key in one of two ways:
|
||||
|
||||
#### Method 1: Database Configuration via UI (Recommended)
|
||||
|
||||
1. Navigate to the AI Agent Chat interface
|
||||
2. If no API key is configured, you'll see a configuration dialog
|
||||
3. Enter your OpenAI API key
|
||||
4. Click **Save Configuration**
|
||||
|
||||
#### Method 2: YAML Configuration
|
||||
|
||||
Add to your `application.yml`:
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
ai:
|
||||
openai:
|
||||
api-key: sk-your-openai-api-key-here
|
||||
```
|
||||
|
||||
### Configuration Priority
|
||||
|
||||
- **Database configuration** takes priority over YAML configuration
|
||||
- Configuration changes are applied immediately without restart
|
||||
- Invalid API keys are automatically validated before saving
|
||||
|
||||
## Using the AI Agent Chat
|
||||
|
||||
### Accessing the Chat Interface
|
||||
|
||||
1. Navigate to the **AI Agent Chat** section in HertzBeat web interface
|
||||
2. You'll see the HertzBeat Monitoring AI Agent welcome screen
|
||||
3. Start typing your questions in the chat input field
|
||||
|
||||
### Sample Conversations
|
||||
|
||||
#### Adding a New Monitor
|
||||
|
||||
```text
|
||||
You: Add a new HTTP monitor for my website https://example.com
|
||||
AI Agent: I'll help you add an HTTP monitor for https://example.com.
|
||||
Let me create that monitor with the following configuration:
|
||||
- Name: example.com Website Monitor
|
||||
- Type: HTTP
|
||||
- Host: example.com
|
||||
- Port: 443 (HTTPS)
|
||||
- Path: /
|
||||
- Interval: 60 seconds
|
||||
|
||||
Monitor created successfully with ID: 12345
|
||||
```
|
||||
|
||||
#### Listing Existing Monitors
|
||||
|
||||
```text
|
||||
You: Show me all my current monitors
|
||||
AI Agent: Here are your current monitors:
|
||||
|
||||
1. **Linux Server** (ID: 1001)
|
||||
- Status: ✅ Online
|
||||
- Host: 192.168.1.100
|
||||
- Last Check: 2 minutes ago
|
||||
|
||||
2. **MySQL Database** (ID: 1002)
|
||||
- Status: ⚠️ Warning
|
||||
- Host: db.example.com
|
||||
- Last Check: 30 seconds ago
|
||||
|
||||
3. **Website Monitor** (ID: 1003)
|
||||
- Status: ✅ Online
|
||||
- Host: https://example.com
|
||||
- Last Check: 1 minute ago
|
||||
|
||||
Would you like details on any specific monitor?
|
||||
```
|
||||
|
||||
#### Getting Monitor Type Information
|
||||
|
||||
```text
|
||||
You: What parameters do I need to monitor a PostgreSQL database?
|
||||
AI Agent: To monitor a PostgreSQL database, you'll need these parameters:
|
||||
|
||||
**Required Parameters:**
|
||||
- **Host**: Database server IP/hostname
|
||||
- **Port**: PostgreSQL port (default: 5432)
|
||||
- **Database**: Database name to connect to
|
||||
- **Username**: Database username
|
||||
- **Password**: Database password
|
||||
|
||||
**Optional Parameters:**
|
||||
- **Timeout**: Connection timeout (default: 6000ms)
|
||||
- **Query Timeout**: SQL query timeout (default: 3000ms)
|
||||
- **SSL**: Enable SSL connection (default: false)
|
||||
|
||||
Would you like me to help you add a PostgreSQL monitor?
|
||||
```
|
||||
|
||||
#### Alert Management
|
||||
|
||||
```text
|
||||
You: Show me all critical alerts from the last 24 hours
|
||||
AI Agent: Here are the critical alerts from the last 24 hours:
|
||||
|
||||
🚨 **Active Alerts (3 firing)**
|
||||
1. **High CPU Usage** - Linux Server (ID: 1001)
|
||||
- Status: 🔥 Firing
|
||||
- Triggered: 2 hours ago
|
||||
- Severity: Critical
|
||||
- Message: CPU usage 95% > threshold 90%
|
||||
|
||||
2. **Database Connection Failed** - MySQL Database (ID: 1002)
|
||||
- Status: 🔥 Firing
|
||||
- Triggered: 45 minutes ago
|
||||
- Severity: Critical
|
||||
- Message: Connection timeout after 10 seconds
|
||||
|
||||
3. **Website Response Time** - Website Monitor (ID: 1003)
|
||||
- Status: ⚠️ Warning
|
||||
- Triggered: 15 minutes ago
|
||||
- Severity: Warning
|
||||
- Message: Response time 5.2s > threshold 3s
|
||||
|
||||
**Alert Summary:**
|
||||
- Total alerts: 12
|
||||
- Firing: 3 (2 critical, 1 warning)
|
||||
- Resolved: 9
|
||||
|
||||
Would you like me to help you investigate any specific alert?
|
||||
```
|
||||
|
||||
#### Metrics Analysis
|
||||
|
||||
```text
|
||||
You: Show me the CPU usage for my Linux server over the last 6 hours
|
||||
AI Agent: Here's the CPU usage data for your Linux server (Monitor ID: 1001) over the last 6 hours:
|
||||
|
||||
📊 **CPU Usage Metrics - Last 6 Hours**
|
||||
```
|
||||
|
||||
Time | Usage (%) | Load Avg | Processes
|
||||
14:00-15:00 | 45.2% | 1.2 | 156
|
||||
15:00-16:00 | 67.8% | 2.1 | 178
|
||||
16:00-17:00 | 89.4% | 3.8 | 195
|
||||
17:00-18:00 | 92.1% | 4.2 | 203
|
||||
18:00-19:00 | 94.7% | 4.8 | 210
|
||||
19:00-20:00 | 88.3% | 3.5 | 188
|
||||
|
||||
``` text
|
||||
|
||||
**Analysis:**
|
||||
- Peak usage: 94.7% at 19:00
|
||||
- Average usage: 79.6%
|
||||
- Trend: High usage sustained for 3+ hours
|
||||
- Current status: Decreasing (88.3%)
|
||||
|
||||
**Real-time metrics:**
|
||||
- Current CPU: 76.2%
|
||||
- Memory usage: 82.1%
|
||||
- Disk I/O: Normal
|
||||
|
||||
Would you like me to check historical data for comparison or investigate the high usage cause?
|
||||
```
|
||||
|
||||
#### Alert Rule Configuration
|
||||
|
||||
```text
|
||||
You: Set up an alert rule for when my website response time exceeds 5 seconds
|
||||
AI Agent: I'll help you create an alert rule for website response time monitoring.
|
||||
|
||||
**Alert Rule Configuration:**
|
||||
- **Monitor Type**: HTTP/Website
|
||||
- **Metric**: Response Time
|
||||
- **Condition**: Greater than 5 seconds
|
||||
- **Severity**: Warning
|
||||
- **Evaluation**: Check every 60 seconds
|
||||
- **Notification**: Email + Dashboard
|
||||
|
||||
**Proposed Rule:**
|
||||
```yaml
|
||||
name: "Website Response Time Alert"
|
||||
condition: "response_time > 5000ms"
|
||||
severity: "warning"
|
||||
description: "Alert when website response time exceeds 5 seconds"
|
||||
evaluation_interval: 60s
|
||||
for: 2m # Alert after condition persists for 2 minutes
|
||||
```
|
||||
|
||||
This rule will:
|
||||
|
||||
1. Monitor response time every minute
|
||||
2. Trigger alert if response time > 5s for 2+ minutes
|
||||
3. Send notifications via configured channels
|
||||
4. Show in dashboard with warning status
|
||||
|
||||
Would you like me to apply this configuration or modify any settings?
|
||||
|
||||
``` text
|
||||
|
||||
### Chat Features
|
||||
|
||||
#### Message History
|
||||
|
||||
- All conversations are automatically saved
|
||||
- Access previous conversations from the sidebar
|
||||
- Search through conversation history
|
||||
- Export conversation logs
|
||||
|
||||
#### Real-time Responses
|
||||
|
||||
- Streaming responses for immediate feedback
|
||||
- Typing indicators show when the assistant is processing
|
||||
- Cancel ongoing requests if needed
|
||||
|
||||
#### Conversation Management
|
||||
|
||||
- Create new conversations for different topics
|
||||
- Rename conversations for better organization
|
||||
- Delete old conversations to keep things clean
|
||||
Note: Conversations are not saved in the database
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Chat Interface Not Loading
|
||||
|
||||
**Symptoms**: Chat interface shows loading spinner indefinitely
|
||||
**Solutions**:
|
||||
|
||||
1. Check browser console for JavaScript errors
|
||||
2. Ensure network connectivity to HertzBeat server
|
||||
|
||||
#### "Service Unavailable" Message
|
||||
|
||||
**Symptoms**: Chat shows "HertzBeat AI monitoring service unavailable"
|
||||
**Solutions**:
|
||||
1.Verify OpenAI API key configuration
|
||||
2.Check application logs for errors
|
||||
3.Ensure database connectivity
|
||||
|
||||
#### Invalid API Key Error
|
||||
|
||||
**Symptoms**: Configuration dialog shows "Invalid API key" error
|
||||
**Solutions**:
|
||||
|
||||
1. Verify your OpenAI API key starts with `sk-`
|
||||
2. Check API key has sufficient credits/quota
|
||||
3. Test API key directly with OpenAI API
|
||||
4. Ensure no extra spaces in the API key
|
||||
|
||||
#### Monitor Creation Failures
|
||||
|
||||
**Symptoms**: AI suggests monitor configuration but creation fails
|
||||
**Solutions**:
|
||||
|
||||
1. Verify you have permissions to create monitors
|
||||
2. Check if monitor with same name already exists
|
||||
3. Ensure target host/service is accessible
|
||||
4. Review monitor parameter validation errors
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable debug logging by setting log level to DEBUG for:
|
||||
|
||||
```yaml
|
||||
logging:
|
||||
level:
|
||||
org.apache.hertzbeat.ai.agent: DEBUG
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Effective Chat Usage
|
||||
|
||||
1. **Be Specific**: "Add HTTP monitor for api.example.com port 8080" vs "add a monitor"
|
||||
2. **Provide Context**: Mention if you want production vs test monitors
|
||||
3. **Ask Follow-ups**: Request configuration details if needed
|
||||
4. **Use Natural Language**: The AI understands conversational requests
|
||||
|
||||
### Monitor Management
|
||||
|
||||
1. **Naming Convention**: Use descriptive monitor names
|
||||
2. **Documentation**: Ask the AI to document complex configurations
|
||||
|
||||
### Security Considerations
|
||||
|
||||
1. **API Key Security**: Store OpenAI API keys securely
|
||||
2. **Access Control**: Limit AI chat access to authorized users
|
||||
3. **Data Privacy**: Be mindful of sensitive information in chat logs
|
||||
4. **Network Security**: Ensure secure connections to OpenAI API
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Bulk Operations
|
||||
|
||||
```text
|
||||
You: Add HTTP monitors for all services in my staging environment:
|
||||
- api-staging.example.com:8080
|
||||
- web-staging.example.com:80
|
||||
- admin-staging.example.com:3000
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- Requires active internet connection for OpenAI API
|
||||
- OpenAI API usage incurs costs based on token consumption
|
||||
- Complex multi-step operations may require multiple interactions
|
||||
- Some advanced configurations may need manual setup
|
||||
- Rate limiting may apply based on OpenAI plan
|
||||
|
||||
## Support
|
||||
|
||||
For issues with AI Agent Chat:
|
||||
|
||||
1. Check this documentation first
|
||||
2. Review application logs for errors
|
||||
3. Test OpenAI API connectivity independently
|
||||
4. Contact HertzBeat support with specific error messages
|
||||
|
||||
---
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
id: apollo
|
||||
title: Apollo Configuration Center
|
||||
sidebar_label: Apollo Configuration Center
|
||||
keywords: [ Open Source Monitoring System, Open Source Middleware Monitoring, Apollo configuration center monitoring ]
|
||||
---
|
||||
|
||||
> Monitoring of general metrics for the Apollo Configuration Center service is performed by calling the Prometheus
|
||||
> interface of the Apollo Configuration Center.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. Set up the Apollo configuration center according to
|
||||
the [deployment documentation](https://www.apolloconfig.com/#/en/deployment/quick-start).
|
||||
2. Visit ```http://${someIp:somePort}/prometheus``` to verify if you can access the metrics data.
|
||||
For more details, please refer
|
||||
to: [Apollo Monitoring Documentation](https://www.apolloconfig.com/#/en/design/apollo-design?id=v-monitoring-related)
|
||||
3. Note⚠️: Starting from version 1.5.0, the Apollo server supports exposing Prometheus-formatted metrics
|
||||
via `/prometheus`.
|
||||
|
||||
### Configuration parameters
|
||||
|
||||
| Parameter Name | Parameter Help Description |
|
||||
|-------------------|-----------------------------------------------------------------------------------------------------------------------|
|
||||
| Target Host | The monitored peer's IPv4, IPv6, or domain name. Note⚠️: Do not include protocol headers (e.g., https://, http://). |
|
||||
| Port | Pulsar's webServicePort value, defaulting to 8080. |
|
||||
| Task Name | The name identifying this monitoring task. The name must be unique. |
|
||||
| Query Timeout | Set the connection timeout in milliseconds (ms). Default is 3000 ms. |
|
||||
| Monitoring Cycle | The interval time for periodic data collection, measured in seconds. The minimum configurable interval is 30 seconds. |
|
||||
| Binding Tags | Used for classifying and managing monitored resources |
|
||||
| Description Notes | Additional identifiers and descriptive notes for this monitoring task. Users can add notes here. |
|
||||
|
||||
### Collection Metrics
|
||||
|
||||
#### Metric Set: Basic Information Metrics
|
||||
|
||||
| Metric Name | Unit | Metric Help Description |
|
||||
|--------------------------------|---------|--------------------------------------------------------------------------|
|
||||
| application_ready_time_seconds | Seconds | Time taken for the application to transition from startup to ready state |
|
||||
| process_uptime_seconds | seconds | Total time the process has been running |
|
||||
| process_cpu_usage | % | Current process CPU usage |
|
||||
|
||||
#### Metric Set: : JVM
|
||||
|
||||
| Metric Name | Unit | Metric Help Description |
|
||||
|-----------------------------------|------|---------------------------------------------------------------|
|
||||
| system_cpu_usage | % | System CPU Usage |
|
||||
| system_load_average_1m | None | System load average over the past minute |
|
||||
| jvm_memory_committed_bytes | MB | Size of memory requested by the JVM from the operating system |
|
||||
| jvm_memory_used_bytes | MB | JVM's current actual memory usage |
|
||||
| jvm_memory_max_bytes | MB | Maximum memory limit available to the JVM |
|
||||
| jvm_gc_pause_seconds_count | None | Total number of JVM GC pause events |
|
||||
| jvm_gc_pause_seconds_sum | None | Total time spent in JVM GC pauses |
|
||||
| jvm_memory_usage_after_gc_percent | None | JVM memory usage after garbage collection |
|
||||
|
||||
#### Metric Set: System Resources
|
||||
|
||||
| Metric Name | Unit | Metric Help Description |
|
||||
|--------------------------|------|--------------------------------------------------------------------|
|
||||
| process_files_max_files | None | Limit on the maximum number of file descriptors a process may open |
|
||||
| process_files_open_files | None | Number of file descriptors currently opened by the process |
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
id: idrac
|
||||
title: Dell iDRAC Monitoring
|
||||
sidebar_label: Server Monitor
|
||||
keywords: [open source monitoring tool, open source server Monitoring, Dell iDRAC Monitoring]
|
||||
---
|
||||
|
||||
> Collect and monitor the general performance Metrics of Dell Server using SNMP Protocol.
|
||||
|
||||
**Protocol: SNMP**
|
||||
|
||||
## Pre-monitoring steps
|
||||
|
||||
1. The target Dell server supports the **SNMP protocol**.
|
||||
2. The **iDRAC** (Integrated Dell Remote Access Controller) has been configured with a network interface, allowing access to the **SNMP port**.
|
||||
3. **SNMP community string** or **user credentials** have been configured with appropriate **permissions**.
|
||||
|
||||
These are basic checks you can follow, and for further details on enabling and configuring SNMP on Dell iDRAC, you can consult the specific user manual of the Dell server.
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
| Parameter Name | Parameter Description |
|
||||
| -------------- |----------------------------------------------------------------------------------------------------------------|
|
||||
| Target Host | The IPv4, IPv6, or domain name of the monitored peer. Note: without protocol header (e.g., https://, http://). |
|
||||
| Port | The port number of the server SNMP, default is 161. |
|
||||
| SNMP Version | Choose between SNMPv1, SNMPv2c, or SNMPv3 |
|
||||
| SNMP Community | SNMP community for v1 v2c |
|
||||
| Username | SNMP username for v3 |
|
||||
| Context Name | SNMP contextName for v3 |
|
||||
| Auth Password | SNMP authPassword for v3 |
|
||||
| Auth Encryption| Authentication encryption method for v3 (MD5 or SHA1) |
|
||||
| Priv Password | SNMP privPassphrase for v3 |
|
||||
| Priv Encryption| Privacy encryption method for v3 (DES or AES128) |
|
||||
|
||||
### Collected Metrics
|
||||
|
||||
#### Metric Set: System Status
|
||||
|
||||
| Metric Name | Unit | Metric Description |
|
||||
| ----------------------- | ---- | ------------------------------------------------------------ |
|
||||
| Global System Status | none | Overall status of the system |
|
||||
| System LCD Status | none | Status of the system LCD |
|
||||
| Global Storage Status | none | Overall status of the storage subsystem |
|
||||
| System Power State | none | Current power state of the system |
|
||||
| System Power Up Time | day | Time since the system was last powered on |
|
||||
|
||||
#### Metric Set: Power Supply
|
||||
|
||||
| Metric Name | Unit | Metric Description |
|
||||
| -------------- | ---- | ------------------------------------------------------------ |
|
||||
| Index | none | Index of the power supply |
|
||||
| Name | none | Name/Location of the power supply |
|
||||
| Wattage | W | Output wattage of the power supply |
|
||||
| Type | none | Type of the power supply (Linear, Switching, Battery, etc.) |
|
||||
| Status | none | Current status of the power supply |
|
||||
|
||||
#### Metric Set: Cooling Devices
|
||||
|
||||
| Metric Name | Unit | Metric Description |
|
||||
| -------------- | ---- | ------------------------------------------------------------ |
|
||||
| Index | none | Index of the cooling device |
|
||||
| Name | none | Name/Location of the cooling device |
|
||||
| Type | none | Type of the cooling device (Fan, Blower, Heat Pipe, etc.) |
|
||||
| Reading | RPM | Current rotation per minute reading |
|
||||
| Status | none | Current status of the cooling device |
|
||||
|
||||
#### Metric Set: Temperature Sensors
|
||||
|
||||
| Metric Name | Unit | Metric Description |
|
||||
| -------------- | ---- | ------------------------------------------------------------ |
|
||||
| Index | none | Index of the temperature sensor |
|
||||
| Name | none | Name/Location of the temperature sensor |
|
||||
| Reading | °C | Current temperature reading |
|
||||
| Status | none | Current status of the temperature sensor |
|
||||
|
||||
#### Metric Set: Voltage Sensors
|
||||
|
||||
| Metric Name | Unit | Metric Description |
|
||||
| -------------- | ---- | ------------------------------------------------------------ |
|
||||
| Index | none | Index of the voltage sensor |
|
||||
| Name | none | Name/Location of the voltage sensor |
|
||||
| Reading | V | Current voltage reading |
|
||||
| Type | none | Type of voltage (3.3V, 5V, 12V, etc.) |
|
||||
| Status | none | Current status of the voltage sensor |
|
||||
|
||||
#### Metric Set: Memory Devices
|
||||
|
||||
| Metric Name | Unit | Metric Description |
|
||||
| -------------- | ---- | ------------------------------------------------------------ |
|
||||
| Index | none | Index of the memory device |
|
||||
| Name | none | Name/Location of the memory device |
|
||||
| Type | none | Type of memory (DDR, DDR2, DDR3, DDR4, etc.) |
|
||||
| Size | G | Size of the memory device |
|
||||
| Status | none | Current status of the memory device |
|
||||
|
||||
#### Metric Set: Processors
|
||||
|
||||
| Metric Name | Unit | Metric Description |
|
||||
| -------------- | ---- | ------------------------------------------------------------ |
|
||||
| Index | none | Index of the processor |
|
||||
| Name | none | Name/Location of the processor |
|
||||
| Speed | MHz | Current speed of the processor |
|
||||
| Family | none | Family of the processor (Pentium, Xeon, Core i7, etc.) |
|
||||
| Status | none | Current status of the processor |
|
||||
@@ -63,9 +63,32 @@ After saving, reload MCP in Cursor or restart the editor.
|
||||
|
||||
### Tools available
|
||||
|
||||
- list_monitors: Returns the list of names of all configured monitors.
|
||||
#### Monitor Management Tools
|
||||
|
||||
More tools are coming soon to expand management and query capabilities.
|
||||
- **query_monitors**: Query existing/configured monitors with comprehensive filtering, pagination, and status overview. Supports filtering by IDs, type, status, host, labels, and sorting.
|
||||
- **add_monitor**: Add a new monitoring target to HertzBeat with comprehensive configuration. Handles different parameter requirements for each monitor type.
|
||||
- **list_monitor_types**: List all available monitor types that can be added to HertzBeat. Shows all supported monitor types with their display names.
|
||||
- **get_monitor_additional_params**: Get the parameter definitions required for a specific monitor type. Shows what parameters are needed when adding a monitor.
|
||||
|
||||
#### Metrics Data Tools
|
||||
|
||||
- **query_realtime_metrics**: Get real-time metrics data for a specific monitor. Returns current metrics values including CPU, memory, disk usage, etc.
|
||||
- **get_historical_metrics**: Get historical metrics data for analysis and trending. Returns time-series data for specified metrics over a time range.
|
||||
- **get_warehouse_status**: Check the status of the metrics storage warehouse system. Returns whether the metrics storage is operational and accessible.
|
||||
|
||||
#### Alert Management Tools
|
||||
|
||||
- **query_alerts**: Query alerts with comprehensive filtering and pagination options. Supports filtering by alert type (single/group), status (firing/resolved), search terms, and sorting.
|
||||
- **get_alerts_summary**: Get alerts summary statistics including total counts, status distribution, and priority breakdown across all monitors.
|
||||
|
||||
#### Alert Rule Definition Tools
|
||||
|
||||
- **create_alert_rule**: Create a HertzBeat alert rule based on app hierarchy structure and user requirements. Supports threshold values, field conditions, and comprehensive alert configuration.
|
||||
- **list_alert_rules**: List existing alert rules with filtering options. Shows configured thresholds and alert definitions with search and pagination.
|
||||
- **get_alert_rule_details**: Get detailed information about a specific alert rule. Shows complete threshold configuration and rule settings.
|
||||
- **toggle_alert_rule**: Enable or disable an alert rule. Allows activating or deactivating threshold monitoring for specific rules.
|
||||
- **get_apps_metrics_hierarchy**: Get the hierarchical structure of all available apps and their metrics for alert rule creation. Returns structured JSON data with field parameters.
|
||||
- **bind_monitors_to_alert_rule**: Bind monitors to an alert rule. Associates specific monitors with alert rules to enable monitoring and alerting.
|
||||
|
||||
### Notes
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
id: risc-v
|
||||
title: HertzBeat Running in RISC-V Architecture Environment
|
||||
sidebar_label: RISC-V
|
||||
keywords: [ Open Source Monitoring System, RISC-V Architecture, RISC-V Running HertzBeat ]
|
||||
---
|
||||
|
||||
> How to Set Up a RISC-V Environment and Run HertzBeat on RISC-V Architecture.
|
||||
|
||||
## RISC-V Environment Setup
|
||||
|
||||
Ensure your RISC-V development environment is properly configured, including compatible hardware or emulators. This guide uses the QEMU emulator on Ubuntu to simulate RISC-V architecture.
|
||||
|
||||
**1. Install QEMU and Required Packages**
|
||||
|
||||
```shell
|
||||
sudo apt update
|
||||
sudo apt install opensbi qemu-system-misc u-boot-qemu
|
||||
```
|
||||
|
||||
**2. Download Ubuntu Image for RISC-V Architecture**
|
||||
|
||||
Download link: <https://ubuntu.com/download/risc-v>
|
||||
Select the `QEMU Emulator` version and download your preferred release.
|
||||
|
||||
**3. Install Ubuntu on QEMU**
|
||||
|
||||
> Use QEMU to boot Ubuntu and emulate RISC-V architecture. Parameters (e.g., boot settings, disk size) can be customized.
|
||||
|
||||
```shell
|
||||
# Decompress the image
|
||||
xz -dk ubuntu-24.04.2-preinstalled-server-riscv64.img.xz
|
||||
|
||||
# Optional: Expand the disk size
|
||||
qemu-img resize -f raw ubuntu-24.04-preinstalled-server-riscv64.img +5G
|
||||
|
||||
# Start the VM using u-boot-qemu
|
||||
qemu-system-riscv64 \
|
||||
-machine virt -nographic -m 2048 -smp 4 \
|
||||
-kernel /usr/lib/u-boot/qemu-riscv64_smode/uboot.elf \
|
||||
-device virtio-net-device,netdev=eth0 -netdev user,id=eth0 \
|
||||
-device virtio-rng-pci \
|
||||
-drive file=ubuntu-24.04.2-preinstalled-server-riscv64.img,format=raw,if=virtio
|
||||
```
|
||||
|
||||
After the console output completes, log in with username ubuntu and default password ubuntu. You will be prompted to set a new password.
|
||||
Reference: [Ubuntu Official Documentation](https://canonical-ubuntu-boards.readthedocs-hosted.com/en/latest/how-to/qemu-riscv/)
|
||||
|
||||
## Install and Configure RISC-V-Compatible JDK
|
||||
|
||||
> Configure a JDK that supports RISC-V architecture. Here we use Temurin JDK 17.
|
||||
|
||||
**1. Download Temurin JDK**
|
||||
Download link: <https://adoptium.net/temurin/releases>
|
||||
|
||||
**2. Configure JDK Environment**
|
||||
|
||||
> Adjust commands based on your downloaded JDK version.
|
||||
|
||||
```shell
|
||||
# Install tools
|
||||
sudo apt update
|
||||
sudo apt install -y tar wget
|
||||
|
||||
# Create installation directory
|
||||
sudo mkdir -p /usr/lib/jvm
|
||||
|
||||
# Extract to system directory
|
||||
sudo tar -xzf OpenJDK17U-jdk_riscv64_linux_hotspot_17.0.15_6.tar.gz -C /usr/lib/jvm
|
||||
|
||||
# Edit environment variables
|
||||
sudo nano /etc/profile.d/java.sh
|
||||
export JAVA_HOME=/usr/lib/jvm/jdk-17.0.15+6
|
||||
export PATH=$JAVA_HOME/bin:$PATH
|
||||
|
||||
# Apply configuration
|
||||
source /etc/profile.d/java.sh
|
||||
|
||||
# Verify installation
|
||||
java -version
|
||||
```
|
||||
|
||||
## Run HertzBeat
|
||||
|
||||
> Download the installer from [HertzBeat Official Site](https://hertzbeat.apache.org/zh-cn/docs/download/) and follow the deployment guide. Example for v1.7.2 .
|
||||
|
||||
```shell
|
||||
cd apache-hertzbeat-1.7.2-incubating-bin/bin/
|
||||
./startup.sh
|
||||
```
|
||||
|
||||
> Notes:
|
||||
>
|
||||
> 1. Replace `OpenJDK17U-jdk_riscv64_linux_hotspot_17.0.15_6.tar.gz` with your actual JDK filename.
|
||||
> 2. Ensure the emulator has internet access to download HertzBeat.
|
||||
> 3. If issues arise, verify disk permissions and Java environment paths.
|
||||
@@ -137,7 +137,7 @@ In an isolated network where multiple networks are not connected, we need to dep
|
||||
* Built on `Java+SpringBoot+TypeScript+Angular` mainstream technology stack , convenient secondary development .
|
||||
* Dev based on HertzBeat need retain copyright.
|
||||
|
||||
**HertzBeat has been included in the [CNCF Observability And Analysis - Monitoring Landscape](https://landscape.cncf.io/card-mode?category=monitoring&grouping=category)**
|
||||
**HertzBeat has been included in the [CNCF Observability And Analysis - Observability Landscape](https://landscape.cncf.io/?view-mode=card&classify=category&sort-by=name&sort-direction=asc#observability-and-analysis--observability)**
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
---
|
||||
|
||||
id: questdb-init
|
||||
|
||||
title: Installation and Initialization of Time-Series Database Service QuestDB (Optional)
|
||||
|
||||
sidebar_label: Metric Data Storage - QuestDB
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
The historical data storage of Apache HertzBeat™ relies on a time-series database. You can choose to install and initialize **one** of the supported databases, or skip the installation (⚠️ However, it is strongly recommended to configure one in the production environment).
|
||||
|
||||
> We recommend using and providing long-term support for VictoriaMetrics as the storage solution.
|
||||
|
||||
QuestDB is an open-source time-series database that stands out in the field of time-series data processing due to its high performance and low latency. We use it to store and analyze the collected historical monitoring metric data.
|
||||
|
||||
**⚠️ Note: Configuring a time-series database is optional, but it is strongly recommended for production environments to ensure more comprehensive historical chart functions, high performance, and stability.**
|
||||
|
||||
**⚠️ If no time-series database is configured, only the historical data of the last hour will be retained.**
|
||||
|
||||
> If you already have an existing QuestDB environment, you can skip directly to the YML configuration step.
|
||||
|
||||
### Install QuestDB
|
||||
<!-- markdownlint-disable MD029 -->
|
||||
1. Download the installation package
|
||||
|
||||
Download the latest version for your operating system from the official GitHub repository:
|
||||
|
||||
```shell
|
||||
# For Linux/macOS (taking v7.3.9 as an example; replace with the latest version number)
|
||||
wget https://github.com/questdb/questdb/releases/download/7.3.9/questdb-7.3.9-no-jre-bin.tar.gz
|
||||
|
||||
# Extract the package
|
||||
tar -zxvf questdb-7.3.9-no-jre-bin.tar.gz
|
||||
mv questdb-7.3.9 /opt/questdb # Move to a common directory
|
||||
```
|
||||
|
||||
For Windows users:
|
||||
|
||||
Download the zip package and extract it to C:\questdb or a custom directory.
|
||||
|
||||
2. Start QuestDB
|
||||
|
||||
```shell
|
||||
# For Linux/macOS: Navigate to the installation directory and start the service
|
||||
cd /opt/questdb/bin
|
||||
./questdb start
|
||||
|
||||
# For Windows (Command Prompt):
|
||||
cd C:\questdb\bin
|
||||
questdb.exe start
|
||||
```
|
||||
|
||||
3. Set up access password
|
||||
|
||||
QuestDB enables authentication through a configuration file, which needs to be modified manually.
|
||||
|
||||
Edit the configuration file:
|
||||
|
||||
```shell
|
||||
# For Linux/macOS
|
||||
vi /opt/questdb/conf/server.conf
|
||||
|
||||
# For Windows
|
||||
notepad C:\questdb\conf\server.conf
|
||||
```
|
||||
|
||||
Enable authentication and configure the password:
|
||||
|
||||
```shell
|
||||
# Enable authentication (disabled by default)
|
||||
http.security.enabled=true
|
||||
pg.security.enabled=true # PostgreSQL protocol authentication
|
||||
|
||||
# Set admin account and password (customize as needed)
|
||||
http.security.admin.username=admin
|
||||
http.security.admin.password=YourStrongPassword123!
|
||||
|
||||
# Optional: Restrict Web Console access to specific IPs (e.g., local access only)
|
||||
http.bind.to=127.0.0.1:9000
|
||||
```
|
||||
|
||||
Restart QuestDB for the changes to take effect:
|
||||
|
||||
```shell
|
||||
# For Linux/macOS
|
||||
./questdb stop
|
||||
./questdb start
|
||||
|
||||
# For Windows
|
||||
questdb.exe stop
|
||||
questdb.exe start
|
||||
```
|
||||
|
||||
4. Configure QuestDB connection in HertzBeat's application.yml file
|
||||
|
||||
Modify HertzBeat's configuration file
|
||||
|
||||
Locate and edit the configuration file at hertzbeat/config/application.yml
|
||||
|
||||
⚠️ Note: For Docker container deployment, you need to mount the application.yml file to the host machine. For the installation package deployment, simply extract the package and modify the file at hertzbeat/config/application.yml.
|
||||
|
||||
**Set the** **warehouse.store.jpa.enabled** **parameter to** **false****, configure the** **warehouse.store.questdb** **data source parameters (HOST, username, password, etc.), and set** **enabled** **to** **true** **to enable QuestDB.**
|
||||
|
||||
```yaml
|
||||
warehouse:
|
||||
store:
|
||||
# Disable the default JPA
|
||||
jpa:
|
||||
enabled: false
|
||||
# Enable QuestDB
|
||||
questdb:
|
||||
enabled: true
|
||||
url: localhost:9000
|
||||
username: admin
|
||||
password: quest
|
||||
```
|
||||
|
||||
Parameter Description:
|
||||
|
||||
| Parameter Name | Description |
|
||||
| -------------- | ------------------------------ |
|
||||
| enabled | Whether to enable QuestDB |
|
||||
| url | QuestDB server URL (host:port) |
|
||||
| username | QuestDB database account |
|
||||
| password | QuestDB database password |
|
||||
|
||||
> **Note:** Due to QuestDB's architectural design, if you need to set an expiration time for data, you can configure it in QuestDB's configuration file server.conf:
|
||||
|
||||
```shell
|
||||
cairo.default.ttl=30d
|
||||
```
|
||||
|
||||
5. Restart HertzBeat
|
||||
<!-- markdownlint-enable MD029 -->
|
||||
@@ -279,16 +279,11 @@ module.exports = {
|
||||
},
|
||||
copyright:
|
||||
`
|
||||
<div style="text-align: left;margin-top:30px">
|
||||
<div style="align-items: center; display: flex">
|
||||
<div style="width: 26%; margin-left: 37%; margin-bottom: 20px; background-color: #282c77; padding: 10px; border-radius: 6px">
|
||||
<a href="https://apache.org/">
|
||||
<img src="/img/icons/asf_logo.svg" alt="Apache logo">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border-top: 1px solid #525252;min-height: 60px;line-height: 25px;text-align: left;font-size: 14px;display: flex;align-items: center;">
|
||||
<div style="text-align: left;margin-top:20px">
|
||||
<a href="https://apache.org/" style="display: flex; align-items: center; justify-content:center">
|
||||
<img src="/img/icons/asf_logo.svg" alt="Apache logo" style="width:auto;height:140px">
|
||||
</a>
|
||||
<div style="margin-top:20px;border-top: 1px solid #525252;min-height: 60px;line-height: 25px;text-align: left;font-size: 14px;display: flex;align-items: center;">
|
||||
<span>
|
||||
Copyright © ${new Date().getFullYear()} The Apache Software Foundation. Apache HertzBeat™, HertzBeat™, and its feather logo are trademarks of The Apache Software Foundation.
|
||||
</span>
|
||||
|
||||
@@ -29,10 +29,9 @@ tags: [opensource, apache]
|
||||
- 灵活的告警阈值规则,`邮件` `Discord` `Slack` `Telegram` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱` 等方式消息及时送达。
|
||||
- 提供强大的状态页构建能力,轻松向用户传达您产品服务的实时状态。
|
||||
|
||||
|
||||
## 2、孵化历程与里程碑
|
||||
|
||||
#### **一、孵化历程**
|
||||
### **一、孵化历程**
|
||||
|
||||
- **2024年4月**:
|
||||
|
||||
@@ -58,14 +57,14 @@ tags: [opensource, apache]
|
||||
|
||||
- 发布 5 个 ASF 合规版本(1.6.0 - 1.7.2)
|
||||
- 特性增强简要:
|
||||
- **多协议监控扩展** - 新增对Ipmi、PLC、NVIDIA、Redfish等关键组件或协议的支持
|
||||
- **智能告警中心** - 实现多表达式告警、周期性阈值和告警抑制/静默/分发等
|
||||
- **动态服务发现** - 通过http_sd和Zookeeper/Nacos实现自动任务发现
|
||||
- **云服务集成** - 告警源集成,支持阿里云SLS、华为云、火山引擎告警源对接
|
||||
- **AI能力增强** - 集成OpenAI等AI平台
|
||||
- **存储优化** - 升级支持更强存储性能
|
||||
- **国际化支持** - 新增日语/繁体中文等多语言界面
|
||||
- **数据处理增强** - 支持SQL/PromQL表达式语法分析,使用Apache Arrow改造数据流动等
|
||||
- **多协议监控扩展** - 新增对Ipmi、PLC、NVIDIA、Redfish等关键组件或协议的支持
|
||||
- **智能告警中心** - 实现多表达式告警、周期性阈值和告警抑制/静默/分发等
|
||||
- **动态服务发现** - 通过http_sd和Zookeeper/Nacos实现自动任务发现
|
||||
- **云服务集成** - 告警源集成,支持阿里云SLS、华为云、火山引擎告警源对接
|
||||
- **AI能力增强** - 集成OpenAI等AI平台
|
||||
- **存储优化** - 升级支持更强存储性能
|
||||
- **国际化支持** - 新增日语/繁体中文等多语言界面
|
||||
- **数据处理增强** - 支持SQL/PromQL表达式语法分析,使用Apache Arrow改造数据流动等
|
||||
|
||||
这些版本的持续迭代,充分彰显 Hertzbeat 对技术的执着追求和对用户需求的高度重视。每一次版本发布,都是 Hertzbeat 成长路上的坚实脚印,为用户带来更强大、更易用的监控体验。
|
||||
|
||||
@@ -78,13 +77,11 @@ tags: [opensource, apache]
|
||||
- **共识导向决策**:面对功能特性增减、技术路线选择等关键决策,社区充分讨论,从多方面权衡。若无法完全统一意见,则投票辅助,保障项目方向契合社区整体利益。
|
||||
|
||||
- **贡献者增长**:
|
||||
- 孵化期间新增 **13 位 Committer** 其中提名 **3 位 PPMC 成员** 且贡献者达到275人,同时核心团队覆盖中美欧多时区。
|
||||
- GitHub **Star 数突破 6K+**,月均活跃开发者达 **30+**。
|
||||
- 孵化期间新增 **13 位 Committer** 其中提名 **3 位 PPMC 成员** 且贡献者达到275人,同时核心团队覆盖中美欧多时区。
|
||||
- GitHub **Star 数突破 6K+**,月均活跃开发者达 **30+**。
|
||||
- **用户生态**:
|
||||
- **使用领域**: 自孵化以来,Apache HertzBeat™ 因其轻量、无代理和高度可扩展的特性,已在多个行业场景中成功落地,成为企业监控基础设施的新选择。
|
||||
- **开发者生态**:社区贡献了大量的监控模板,覆盖 Kubernetes、Docker、Redis、MySQL 等主流技术栈,极大降低了监控配置成本。
|
||||
|
||||
|
||||
- **使用领域**: 自孵化以来,Apache HertzBeat™ 因其轻量、无代理和高度可扩展的特性,已在多个行业场景中成功落地,成为企业监控基础设施的新选择。
|
||||
- **开发者生态**:社区贡献了大量的监控模板,覆盖 Kubernetes、Docker、Redis、MySQL 等主流技术栈,极大降低了监控配置成本。
|
||||
|
||||
## 3、 未来展望和规划
|
||||
|
||||
@@ -110,8 +107,6 @@ tags: [opensource, apache]
|
||||
- 扩大社区规模,吸引更多贡献者参与项目开发
|
||||
- 完善多语言支持,包括文档国际化和界面本地化
|
||||
|
||||
|
||||
|
||||
## 4、致谢
|
||||
|
||||
Apache HertzBeat™ 能够成功从孵化器毕业,离不开所有社区成员与支持者的共同努力。在此,我们谨致以最诚挚的感谢:
|
||||
@@ -140,4 +135,4 @@ Apache HertzBeat™ 能够成功从孵化器毕业,离不开所有社区成员
|
||||
|
||||
欢迎更多开发者加入我们,玩得开心!
|
||||
|
||||
—— Apache HertzBeat™ 社区 敬上
|
||||
—— Apache HertzBeat™ 社区 敬上
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
---
|
||||
title: Apache HertzBeat™ 1.7.3 发布公告
|
||||
author: LiuTianyou
|
||||
author_title: LiuTianyou
|
||||
author_url: https://github.com/LiuTianyou
|
||||
tags: [opensource, release]
|
||||
keywords: [open source monitoring system, alerting system, Hertzbeat, release]
|
||||
---
|
||||
|
||||
亲爱的社区小伙伴们,
|
||||
|
||||
我们很高兴地宣布 Apache HertzBeat™ 1.7.3 版本正式发布,这也是 Apache HertzBeat™ 毕业成为 Apache 顶级项目的第一个版本。
|
||||
|
||||
## Downloads and Documentation
|
||||
|
||||
- **Apache HertzBeat™ 1.7.3 Download Link**: <https://hertzbeat.apache.org/zh-cn/docs/download>
|
||||
- **Apache HertzBeat™ Documentation**: <https://hertzbeat.apache.org/zh-cn/docs/>
|
||||
|
||||
## Major Updates
|
||||
|
||||
### New Features and Enhancements
|
||||
|
||||
- **监控支持扩展**:新增对 Apache DolphinScheduler(#3656)、TDengine(#3678)、macOS 系统(#3715)、Synology NAS(#3721) 、HertzBeat(#3641)的监控支持,Oracle 监控增加用户密码相关监控指标(#3674)。
|
||||
- **核心功能增强**:支持周期性阈值预览(#3505),支持导出所有监控(#3509),监控模板支持 JSON 嵌套解析(#3645),HTTP 采集支持使用 JsonPath 解析数字类型数据(#3612), VictoriaMetrics 指标支持添加自定义标签(#3622)。
|
||||
- **国际化 (i18n)**:支持采集器上线通知的国际化(#3585)。
|
||||
- **更多新功能**
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Grafana集成问题**:修复 Grafana 集成可视化展示问题。(#3666)。
|
||||
- **系统稳定性**:修复 VictoriaMetrics 存储未配置时启动报错的问题(#3552),修复 Prometheus 解析中输入流过早关闭的问题(#3567),修复采集器下线时不发送下线通知的问题(#3601)。
|
||||
- **数据解析问题**:修复 collector 的 AES 密钥解码错误的问题(#3652),修复 JDBC 配置参数失效问题(#3625),修复因 Jexl 关键字冲突导致多个监控模板解析失败的问题(#3629, #3632, #3685, #3686, #3687, #3693, #3705, #3707, #3708)。
|
||||
- **和其它的BUG修复**
|
||||
|
||||
### Refactoring and Optimization
|
||||
|
||||
- **性能优化**:优化监控列表页加载性能(#3719),优化批量刷新任务的调度逻辑(#3660),VictoriaMetrics 写入请求启用 Gzip 压缩(#3595)。
|
||||
- **逻辑重构与改进**:重构告警缓存管理器 (AlarmCacheManager) 的处理逻辑(#3525),重构 Prometheus 标签的解析逻辑和 PromQL 生成逻辑(#3662, #3725),优化告警判断逻辑的比较器(#3574)。
|
||||
- **UI/UX 优化**:优化状态页面布局和 UI(#3680),优化状态页事件列表(#3727),优化首页导航栏布局(#3616),优化新建监控页面交互效果(#3726),更新 UI 主题。(#3682),优化监控数据图表组件。(#3668)
|
||||
- **和其它的优化**
|
||||
|
||||
### Tests and Quality
|
||||
|
||||
- **单元测试覆盖**:为 VictoriaMetrics 数据存储添加单元测试(#3595)。
|
||||
- **e2e测试**:提升 Kafka 端到端 (e2e) 测试质量(#3520)。
|
||||
- **测试效率**:实现并行 CI 以加快构建过程(#3523)。
|
||||
- **和其它的测试**
|
||||
|
||||
### Documentation Enhancements
|
||||
|
||||
- **国际化文档**:由 @Calvin979 主导,添加了大量监控模板的日语文档 (i18n),覆盖了 HPE Switch, Huawei Switch, Kafka, JVM, Linux, MySQL, Redis, SpringBoot 等数十个监控模板。
|
||||
- **内容修正与优化**:修复文档中的错误、失效链接和语义问题(#3526, #3559, #3608, #3695)。
|
||||
- **社区生态**:添加新 committer 和 PMC 成员介绍博客(#3522, #3561, #3563, #3569),更新项目毕业相关的文档和网站(#3584, #3692, #3701, #3729),更新贡献者文档。(#3667)
|
||||
- **更多的文档更新**
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
感谢以下社区成员的共同努力:
|
||||
|
||||
> @pjfanning @chingjustwe @delei @rowankid @MasamiYui @tomsun28 @Aias00 @zhangshenghang @zqr10159 @LiuTianyou @yy549159265 @lx1229 @VampireAchao
|
||||
> @cto-huhang @pwallk @bigcyy @sarthakeash @Carpe-Wang @YxYL6125 @tuzuy @lynx009 @Duansg @carlpinto25 @Calvin979 @Cyanty @Saramanda9988
|
||||
|
||||
## What's Changed
|
||||
|
||||
```markdown
|
||||
* [release] release new version 1.7.2 by @tomsun28 in #3510
|
||||
* [doc] japanese hpe switch by @Calvin979 in #3511
|
||||
* [doc] japanese huawei switch by @Calvin979 in #3515
|
||||
* [doc] japanese hive by @Calvin979 in #3506
|
||||
* [doc] japanese hugeGraph by @Calvin979 in #3518
|
||||
* [feature] Support for Periodic Threshold preview. by @Duansg in #3505
|
||||
* [Improve] Improve kafka e2e by @zhangshenghang in #3520
|
||||
* [doc] japanese iceberg by @Calvin979 in #3519
|
||||
* [fix] Update the correct replacement of outdated functions and placeholders by @Cyanty in #3516
|
||||
* [doc] japanese influxdb by @Calvin979 in #3521
|
||||
* [doc] japanese hikvision by @Calvin979 in #3499
|
||||
* [doc] Add new commiter blog by @MasamiYui in #3522
|
||||
* [fix] fix i18n error and append two metrics about disk infos by @lx1229 in #3514
|
||||
* [feat]parallel ci by @Aias00 in #3523
|
||||
* [doc] japanese influxdb promql by @Calvin979 in #3527
|
||||
* fix: fix something by @Aias00 in #3524
|
||||
* [doc] japanese iotdb by @Calvin979 in #3530
|
||||
* download version deal by @lynx009 in #3531
|
||||
* [doc] update outdated links by @lynx009 in #3535
|
||||
* [doc]update download page text by @zqr10159 in #3532
|
||||
* [doc] japanese ipmi by @Calvin979 in #3537
|
||||
* [doc] japanese jetty by @Calvin979 in #3539
|
||||
* enable GitHub Dependabot to create PRs for security alerts but not for every dependency update by @pjfanning in #3543
|
||||
* [doc] Fix some issues in the documentation by @Duansg in #3526
|
||||
* [doc] recommend newer mysql connector jar by @pjfanning in #3541
|
||||
* [doc] add hertzbeat maturity and fix doc by @tomsun28 in #3544
|
||||
* [doc] update download page by @lynx009 in #3549
|
||||
* [doc] japanese kafka by @Calvin979 in #3548
|
||||
* [doc] japanese jvm by @Calvin979 in #3545
|
||||
* [doc] japanese kafka client by @Calvin979 in #3550
|
||||
* [feature] Support export all allmonitors by @YxYL6125 in #3509
|
||||
* feat: Add LogUtil wrapper and optimize logging comments by @Carpe-Wang in #3489
|
||||
* [refactor] AlarmCacheManager refactoring processing logic by @Duansg in #3525
|
||||
* [improve] make jackson serialize all field visibility by @tomsun28 in #3551
|
||||
* [bugfix] fix npe due the victoria insert not config when startup by @tomsun28 in #3552
|
||||
* [doc] V1.7.2 version blog by @lynx009 in #3558
|
||||
* [doc] japanese kafka promql by @Calvin979 in #3553
|
||||
* [doc] Fix documentation error and adjust Chinese semantics by @Duansg in #3559
|
||||
* [doc] add new pmc and committer by @tomsun28 in #3561
|
||||
* [doc] japanese kingbase by @Calvin979 in #3562
|
||||
* [doc] add new committer by @MasamiYui in #3563
|
||||
* [feat] Add QueryExecutor does not support internationalization. by @Duansg in #3565
|
||||
* [doc] japanese kubernetes by @Calvin979 in #3568
|
||||
* [mqtt] refact the MQTT based on the Paho SDK and support both unidirectional and bidirectional MQTT over TLS connections. by @yy549159265 in #3474
|
||||
* [docs] Add new committer blog by @bigcyy in #3569
|
||||
* [bugfix] Fix MySQL WrongArgumentException by @pwallk in #3564
|
||||
* [doc] japanese kvrocks by @Calvin979 in #3570
|
||||
* [doc] japanese linux by @Calvin979 in #3571
|
||||
* [fix]: Fix a problem where the inputstream was closed prematurely when parsing Prometheus by @zqr10159 in #3567
|
||||
* [doc]: add FAQs about task limits by @zqr10159 in #3581
|
||||
* [infra] upgrade openjdk image to fix Debian source failed by @tomsun28 in #3587
|
||||
* [feature] Support i18n of collector online notification by @Duansg in #3585
|
||||
* [doc] add japanese i18n in app-linux_script.yml by @Calvin979 in #3575
|
||||
* [doc] add japanese i18n in app-mariadb.yml by @Calvin979 in #3577
|
||||
* [doc] add japanese i18n in app-memcached.yml by @Calvin979 in #3578
|
||||
* [doc] add japanese i18n in app-modbus.yml by @Calvin979 in #3580
|
||||
* [feature] System time zone optimization. by @Duansg in #3588
|
||||
* [doc] add japanese i18n in app-mongodb.yml by @Calvin979 in #3590
|
||||
* [refactor] Refactor the comparison logic by @Duansg in #3574
|
||||
* [fix] Fix collector online metadata filling by @Duansg in #3579
|
||||
* [doc] add japanese i18n in app-mongodb_atlas.yml by @Calvin979 in #3593
|
||||
* [doc] add japanese i18n in app-mysql.yml by @cto-huhang in #3594
|
||||
* [doc] add japanese i18n in app-zookeeper.yml and app-zookeeper_sd.yml by @Cyanty in #3596
|
||||
* [doc] add japanese i18n in app-nginx.yml by @Duansg in #3600
|
||||
* [doc] add japanese i18n in app-rocketmq.yml and app-rabbitmq.yml by @Saramanda9988 in #3598
|
||||
* [improve] VM write request sets up gzip compression and adds saveData unit tests by @Cyanty in #3595
|
||||
* [feat] Auto-generate AES key if not configured by @bigcyy in #3604
|
||||
* [doc] update japanese i18n in app-mqtt.yml and app-mysql.yml by @Calvin979 in #3597
|
||||
* [fix] Fix collector offline without sending offline notification by @Duansg in #3601
|
||||
* [doc] fix image reference issues in documents by @delei in #3608
|
||||
* [doc] add japanese i18n in app-nacos.yml by @cto-huhang in #3599
|
||||
* [doc] add japanese i18n in app-nvidia.yml, app-ping.yml, app-pop3.yml, app-port.yml and app-push.yml by @Calvin979 in #3611
|
||||
* [doc] update japanese i18n in app-nacos_sd.yml, app-nebula_graph.yml, app-zookeeper.yml by @Calvin979 in #3602
|
||||
* [doc] add japanese i18n in app-nebula_graph_cluster.yml and update japanese of client by @Calvin979 in #3603
|
||||
* [bugfix] the tagValue may be null in the determineNewLabels method by @delei in #3606
|
||||
* [doc] add japanese i18n in app-netease_mailbox.yml and app-ntp.yml by @Calvin979 in #3607
|
||||
* [doc] add japanese i18n in app-oceanbase.yml by @VampireAchao in #3614
|
||||
* [feat] Http collect supports jsonpath parsing of numeric type by @Duansg in #3612
|
||||
* [doc] Optimize the home website navbar layout UI for medium-sized screens by @VampireAchao in #3616
|
||||
* [doc] add japanese i18n in app-opensuse.yml by @Calvin979 in #3619
|
||||
* [doc] add japanese i18n in app-oracle.yml by @Calvin979 in #3621
|
||||
* [doc] add japanese i18n in app-postgresql.yml by @Calvin979 in #3624
|
||||
* [doc] add linux operating system practice usecase by @delei in #3628
|
||||
* [fix] Fixed springboot3abnormal use of the keyword jexl by @Duansg in #3629
|
||||
* [Feature] add customized labels to VM metrics by @chingjustwe in #3622
|
||||
* [fix] Fixed issue where jdbc url was replaced by @Duansg in #3625
|
||||
* [fix] remove optional metrics by @Duansg in #3636
|
||||
* [doc] add japanese i18n in app-openai.yml and app-opengauss.yml by @Calvin979 in #3617
|
||||
* [doc] add japanese i18n in app-smtp.yml, app-ssl_cert.yml, app-storm.yml, app-udp_port.yml, app-uniview.yml, app-website.yml, app-websocket.yml by @Calvin979 in #3643
|
||||
* [bugfix] fix potential StringIndexOutOfBoundsException by @delei in #3642
|
||||
* [feat] Monitoring templates support JSON nested parsing. by @Duansg in #3645
|
||||
* [doc] add English version of linux operating system practice usecase by @delei in #3631
|
||||
* [bugfix] fixed jexl keyword issue in springboot2 monitoring template by @delei in #3632
|
||||
* [doc] add japanese i18n in app-qq_mailbox.yml and app-redfish.yml by @Calvin979 in #3635
|
||||
* [doc] add japanese i18n in app-prometheus.yml, app-pulsar.yml and app-registry.yml by @Calvin979 in #3639
|
||||
* [doc] add japanese i18n in app-s7.yml, app-tplink_switch.yml, app-shenyu.yml by @Calvin979 in #3648
|
||||
* [feature] Support HertzBeat self monitor by @zqr10159 in #3641
|
||||
* [fix] Fixed an issue where template configuration was overwritten by @Duansg in #3649
|
||||
* [bugfix] fix the collector aes decode with secret error by @tomsun28 in #3652
|
||||
* [doc] add japanese i18n in app-tidb.yml, app-windows_script.yml by @Calvin979 in #3650
|
||||
* [doc] add japanese i18n in app-springboot3.yml by @Duansg in #3658
|
||||
* [doc] improve documentation formatting and readability by @delei in #3659
|
||||
* [doc] add japanese i18n in app-prestodb.yml, app-spark.yml by @Calvin979 in #3651
|
||||
* [doc] add japanese i18n in app-process.yml by @Calvin979 in #3654
|
||||
* [doc] add japanese i18n in app-spring_gateway.yml, app-sqlserver.yml by @Calvin979 in #3661
|
||||
* [doc] add japanese i18n in app-redhat.yml by @Calvin979 in #3655
|
||||
* [doc] add japanese i18n and remove chinese in script folder by @Calvin979 in #3665
|
||||
* [doc] update contribution doc by @tomsun28 in #3667
|
||||
* [GSOC] MCP server setup, authorization, and basic tool support by @sarthakeash in #3610
|
||||
* [doc] add japanese i18n in app-redis.yml by @Calvin979 in #3669
|
||||
* [Feature] add Apache DolphinScheduler monitoring support by @delei in #3656
|
||||
* [improve] Optimize the scheduling logic for batch flush tasks by @Cyanty in #3660
|
||||
* [fix] Fixed Grafana visualization integration display issue by @Duansg in #3666
|
||||
* [feature] add user password monitoring metrics in oracle monitor by @delei in #3674
|
||||
* [doc] add japanese i18n in app-redis_cluster.yml by @Calvin979 in #3672
|
||||
* [doc] add japanese i18n in app-dolphinscheduler.yml by @Calvin979 in #3677
|
||||
* [improve] optimize status page layout and UI by @delei in #3680
|
||||
* [doc] add japanese i18n in app-redis_sentinel.yml by @Calvin979 in #3681
|
||||
* [doc] remove all (incubating) by @VampireAchao in #3584
|
||||
* [bugfix] fixed jexl keyword issue in postgresql monitoring template by @delei in #3685
|
||||
* [bugfix] fixed jexl keyword issue in hertzbeat monitoring template by @delei in #3686
|
||||
* [bugfix] fixed jexl keyword issue in windows monitoring template by @delei in #3687
|
||||
* [bugfix] fixed jexl keyword issue in spring_gateway monitoring template by @delei in #3693
|
||||
* [doc] update doc while graduate by @tomsun28 in #3692
|
||||
* [doc] Fixed menu path errors and MD document format anomalies. by @Duansg in #3695
|
||||
* chore: update ui theme by @tomsun28 in #3682
|
||||
* [doc] add japanese i18n in app-rockylinux.yml by @Calvin979 in #3688
|
||||
* [doc] add japanese i18n in app-seatunnel.yml by @Calvin979 in #3691
|
||||
* [doc] add japanese i18n in app-tomcat.yml by @Calvin979 in #3700
|
||||
* [refactor] modify message column to TEXT type by @delei in #3698
|
||||
* [doc] update website, doc while graduate by @tomsun28 in #3701
|
||||
* [bugfix] resolve translation value fetching for uppercase app names by @rowankid in #3690
|
||||
* [doc] add japanese i18n in app-vastbase.yml by @Calvin979 in #3704
|
||||
* [bugfix] fixed jexl keyword issue in kingbase monitoring template by @delei in #3705
|
||||
* [feat] Support TDengine monitoring by @Duansg in #3678
|
||||
* [bugfix] fixed jexl keyword issue in greenplum monitoring template by @delei in #3707
|
||||
* [bugfix] fixed jexl keyword issue in vastbase monitoring template by @delei in #3708
|
||||
* refactor: Optimize the monitoring data chart component by @tuzuy in #3668
|
||||
* [improve] Improve the parsing logic of prometheus label by @Duansg in #3662
|
||||
* [doc] modify github link. by @lynx009 in #3714
|
||||
* [doc] add japanese i18n in app-ubuntu.yml, app-windows.yml and app-yarn.yml by @Calvin979 in #3703
|
||||
* [doc] add japanese i18n in app-valkey.yml by @Calvin979 in #3706
|
||||
* [improve] Improve performance issues when loading large numbers of metric cards in Monitors by @Duansg in #3719
|
||||
* [doc] add japanese i18n in app-tdengine.yml by @Calvin979 in #3717
|
||||
* [feature] add macOS monitoring support by @delei in #3715
|
||||
* [feature] add Synology NAS monitoring support by @delei in #3721
|
||||
* [improve] Improve Prometheus label PromQL parsing logic by @Duansg in #3725
|
||||
* [release] release new version 1.7.3 by @LiuTianyou in #3724
|
||||
* [Improve] changed scroll of intervals in New Monitor page by @carlpinto25 in #3726
|
||||
* [improve] optimize the incident list on the status page to support paginated queries by @delei in #3727
|
||||
* docs: update doc and add graduation blog by @tomsun28 in #3729
|
||||
```
|
||||
|
||||
## New Contributors
|
||||
|
||||
- @yy549159265 made their first contribution in <https://github.com/apache/hertzbeat/pull/3474>
|
||||
- @cto-huhang made their first contribution in <https://github.com/apache/hertzbeat/pull/3594>
|
||||
- @Saramanda9988 made their first contribution in <https://github.com/apache/hertzbeat/pull/3598>
|
||||
- @delei made their first contribution in <https://github.com/apache/hertzbeat/pull/3608>
|
||||
- @chingjustwe made their first contribution in <https://github.com/apache/hertzbeat/pull/3622>
|
||||
- @rowankid made their first contribution in <https://github.com/apache/hertzbeat/pull/3690>
|
||||
- @tuzuy made their first contribution in <https://github.com/apache/hertzbeat/pull/3668>
|
||||
- @carlpinto25 made their first contribution in <https://github.com/apache/hertzbeat/pull/3726>
|
||||
|
||||
## Apache Hertzbeat
|
||||
|
||||
**仓库地址:**
|
||||
|
||||
<https://github.com/apache/hertzbeat>
|
||||
|
||||
**网址:**
|
||||
|
||||
<https://hertzbeat.apache.org/>
|
||||
|
||||
**Apache HertzBeat™ 下载地址:**
|
||||
|
||||
<https://hertzbeat.apache.org/zh-cn/docs/download>
|
||||
|
||||
**Apache HertzBeat™ Docker 镜像版本:**
|
||||
|
||||
> Apache HertzBeat™ 为每个版本制作了 Docker 镜像. 你可以从 Docker Hub 拉取使用.
|
||||
|
||||
- HertzBeat <https://hub.docker.com/r/apache/hertzbeat>
|
||||
- HertzBeat Collector <https://hub.docker.com/r/apache/hertzbeat-collector>
|
||||
|
||||
**Apache HertzBeat™ 开源社区如何参与?**
|
||||
|
||||
<https://hertzbeat.apache.org/zh-cn/docs/community/contribution>
|
||||
@@ -147,7 +147,7 @@ git pull upstream master
|
||||
|
||||
在您的 PR 被合并后,您可以发送如下内容邮件到 `dev@hertzbeat.apache.org` 来申请领取属于您的 Apache HertzBeat™ 贡献者电子证书。
|
||||
|
||||
```
|
||||
```text
|
||||
Title: [Contributor] Request for a Contributor Certificate: <Your Full Name>
|
||||
Body:
|
||||
Hello,
|
||||
@@ -162,7 +162,7 @@ Best regards,
|
||||
<Your Name>
|
||||
```
|
||||
|
||||
在几个工作日内,您会收到携带电子证书的回复,您的名字也会出现在贡献者列表中。
|
||||
在几个工作日内,您会收到携带电子证书的回复,您的名字也会出现在贡献者列表中。
|
||||
注意贡献者的证书是电子的,待您成为 `Committer` 或者 `PMC` 的证书是实体的。
|
||||
|
||||
证书样例:
|
||||
|
||||
+67
-166
@@ -4,7 +4,7 @@ title: 如何进行版本发布
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
本教程详细描述了如何发布Apache HertzBeat,并以 v1.6.0 版本的发布为例。
|
||||
本教程详细描述了如何发布Apache HertzBeat,并以 v1.7.3 版本的发布为例。
|
||||
|
||||
## 1. 环境要求
|
||||
|
||||
@@ -133,8 +133,8 @@ gpg: unchanged: 1
|
||||
|
||||
#### 2.4 将 gpg 公钥添加到 Apache SVN 项目仓库的 KEYS 文件中
|
||||
|
||||
- Apache HertzBeat Dev 分支 <https://dist.apache.org/repos/dist/dev/incubator/hertzbeat>
|
||||
- Apache HertzBeat Release 分支 <https://dist.apache.org/repos/dist/release/incubator/hertzbeat>
|
||||
- Apache HertzBeat Dev 分支 <https://dist.apache.org/repos/dist/dev/hertzbeat>
|
||||
- Apache HertzBeat Release 分支 <https://dist.apache.org/repos/dist/release/hertzbeat>
|
||||
|
||||
##### 2.4.1 将公钥添加到dev分支的KEYS
|
||||
|
||||
@@ -142,7 +142,7 @@ gpg: unchanged: 1
|
||||
$ mkdir -p svn/dev
|
||||
$ cd svn/dev
|
||||
|
||||
$ svn co https://dist.apache.org/repos/dist/dev/incubator/hertzbeat
|
||||
$ svn co https://dist.apache.org/repos/dist/dev/hertzbeat
|
||||
$ cd svn/dev/hertzbeat
|
||||
|
||||
# 将生成的KEY追加到KEYS文件中,检查是否添加正确
|
||||
@@ -157,7 +157,7 @@ $ svn ci -m "add gpg key for muchunjin"
|
||||
$ mkdir -p svn/release
|
||||
$ cd svn/release
|
||||
|
||||
$ svn co https://dist.apache.org/repos/dist/release/incubator/hertzbeat
|
||||
$ svn co https://dist.apache.org/repos/dist/release/hertzbeat
|
||||
$ cd svn/release/hertzbeat
|
||||
|
||||
# 将生成的KEY追加到KEYS文件中,检查是否添加正确
|
||||
@@ -170,17 +170,17 @@ $ svn ci -m "add gpg key for muchunjin"
|
||||
|
||||
### 准备发布物料
|
||||
|
||||
#### 3.1 基于 master 分支,创建一个名为 release-${release_version}-rcx 的分支,例如 release-1.6.0-rc1。并基于 release-1.6.0-rc1 分支创建一个名为 v1.6.0-rc1 的标签,并将此标签设置为预发布
|
||||
#### 3.1 基于 master 分支,创建一个名为 release-${release_version}-rcx 的分支,例如 release-1.7.3-rc1。并基于 release-1.7.3-rc1 分支创建一个名为 v1.7.3-rc1 的标签,并将此标签设置为预发布
|
||||
|
||||
```shell
|
||||
git checkout master
|
||||
git checkout -b release-1.6.0-rc1
|
||||
git checkout -b release-1.7.3-rc1
|
||||
```
|
||||
|
||||
#### 3.2 本地切换到待发布分支
|
||||
|
||||
```shell
|
||||
git checkout release-1.6.0-rc1
|
||||
git checkout release-1.7.3-rc1
|
||||
```
|
||||
|
||||
#### 3.3 编译二进制包
|
||||
@@ -213,9 +213,9 @@ mvn clean package -Pcluster
|
||||
|
||||
生成的二进制包在:
|
||||
|
||||
- `dist/apache-hertzbeat-{version}-incubating-bin.tar.gz`
|
||||
- `dist/apache-hertzbeat-collector-{version}-incubating-bin.tar.gz`
|
||||
- `dist/apache-hertzbeat-{version}-incubating-docker-compose.tar.gz`
|
||||
- `dist/apache-hertzbeat-{version}-bin.tar.gz`
|
||||
- `dist/apache-hertzbeat-collector-{version}-bin.tar.gz`
|
||||
- `dist/apache-hertzbeat-{version}-docker-compose.tar.gz`
|
||||
|
||||
#### 3.4 打包项目源代码
|
||||
|
||||
@@ -224,12 +224,12 @@ mvn clean package -Pcluster
|
||||
```shell
|
||||
git archive \
|
||||
--format=tar.gz \
|
||||
--output="dist/apache-hertzbeat-1.6.0-incubating-src.tar.gz" \
|
||||
--prefix=apache-hertzbeat-1.6.0-incubating-src/ \
|
||||
release-1.6.0-rc1
|
||||
--output="dist/apache-hertzbeat-1.7.3-src.tar.gz" \
|
||||
--prefix=apache-hertzbeat-1.7.3-src/ \
|
||||
release-1.7.3-rc1
|
||||
```
|
||||
|
||||
生成的代码归档文件在 `dist/apache-hertzbeat-1.6.0-incubating-src.tar.gz`
|
||||
生成的代码归档文件在 `dist/apache-hertzbeat-1.7.3-src.tar.gz`
|
||||
|
||||
### 签名发布物料
|
||||
|
||||
@@ -254,18 +254,18 @@ for i in *.tar.gz; do echo $i; sha512sum $i > $i.sha512 ; done
|
||||
> 最终文件列表如下
|
||||
|
||||
```text
|
||||
apache-hertzbeat-1.6.0-incubating-src.tar.gz
|
||||
apache-hertzbeat-1.6.0-incubating-src.tar.gz.asc
|
||||
apache-hertzbeat-1.6.0-incubating-src.tar.gz.sha512
|
||||
apache-hertzbeat-1.6.0-incubating-bin.tar.gz
|
||||
apache-hertzbeat-1.6.0-incubating-bin.tar.gz.asc
|
||||
apache-hertzbeat-1.6.0-incubating-bin.tar.gz.sha512
|
||||
apache-hertzbeat-1.6.0-incubating-docker-compose.tar.gz
|
||||
apache-hertzbeat-1.6.0-incubating-docker-compose.tar.gz.asc
|
||||
apache-hertzbeat-1.6.0-incubating-docker-compose.tar.gz.sha512
|
||||
apache-hertzbeat-collector-1.6.0-incubating-bin.tar.gz
|
||||
apache-hertzbeat-collector-1.6.0-incubating-bin.tar.gz.asc
|
||||
apache-hertzbeat-collector-1.6.0-incubating-bin.tar.gz.sha512
|
||||
apache-hertzbeat-1.7.3-src.tar.gz
|
||||
apache-hertzbeat-1.7.3-src.tar.gz.asc
|
||||
apache-hertzbeat-1.7.3-src.tar.gz.sha512
|
||||
apache-hertzbeat-1.7.3-bin.tar.gz
|
||||
apache-hertzbeat-1.7.3-bin.tar.gz.asc
|
||||
apache-hertzbeat-1.7.3-bin.tar.gz.sha512
|
||||
apache-hertzbeat-1.7.3-docker-compose.tar.gz
|
||||
apache-hertzbeat-1.7.3-docker-compose.tar.gz.asc
|
||||
apache-hertzbeat-1.7.3-docker-compose.tar.gz.sha512
|
||||
apache-hertzbeat-collector-1.7.3-bin.tar.gz
|
||||
apache-hertzbeat-collector-1.7.3-bin.tar.gz.asc
|
||||
apache-hertzbeat-collector-1.7.3-bin.tar.gz.sha512
|
||||
```
|
||||
|
||||
#### 3.6 验证签名
|
||||
@@ -276,30 +276,34 @@ $ cd dist
|
||||
# 验证签名
|
||||
$ for i in *.tar.gz; do echo $i; gpg --verify $i.asc $i ; done
|
||||
|
||||
apache-hertzbeat-1.6.0-incubating-src.tar.gz
|
||||
apache-hertzbeat-1.7.3-bin.tar.gz
|
||||
gpg: Signature made Tue May 2 12:16:35 2023 CST
|
||||
gpg: using RSA key 85778A4CE4DD04B7E07813ABACFB69E705016886
|
||||
gpg: Good signature from "muchunjin (apache key) <muchunjin@apache.org>" [ultimate]
|
||||
apache-hertzbeat-1.6.0-incubating-bin.tar.gz
|
||||
apache-hertzbeat-1.7.3-docker-compose.tar.gz
|
||||
gpg: Signature made Tue May 2 12:16:36 2023 CST
|
||||
gpg: using RSA key 85778A4CE4DD04B7E07813ABACFB69E705016886
|
||||
gpg: Good signature from "muchunjin (apache key) <muchunjin@apache.org>" [ultimate]
|
||||
apache-hertzbeat-collector-1.6.0-incubating-bin.tar.gz
|
||||
apache-hertzbeat-1.7.3-src.tar.gz
|
||||
gpg: Signature made Tue May 2 12:16:37 2023 CST
|
||||
gpg: using RSA key 85778A4CE4DD04B7E07813ABACFB69E705016886
|
||||
gpg: BAD signature from "muchunjin (apache key) <muchunjin@apache.org>" [ultimate]
|
||||
gpg: Good signature from "muchunjin (apache key) <muchunjin@apache.org>" [ultimate]
|
||||
apache-hertzbeat-collector-1.7.3-bin.tar.gz
|
||||
gpg: Signature made Tue May 2 12:16:37 2023 CST
|
||||
gpg: using RSA key 85778A4CE4DD04B7E07813ABACFB69E705016886
|
||||
gpg: Good signature from "muchunjin (apache key) <muchunjin@apache.org>" [ultimate]
|
||||
|
||||
# 验证 SHA512
|
||||
$ for i in *.tar.gz; do echo $i; sha512sum --check $i.sha512; done
|
||||
|
||||
apache-hertzbeat-1.6.0-incubating-src.tar.gz
|
||||
apache-hertzbeat-1.6.0-incubating-src.tar.gz: OK
|
||||
apache-hertzbeat-1.6.0-incubating-bin.tar.gz
|
||||
apache-hertzbeat-1.6.0-incubating-bin.tar.gz: OK
|
||||
apache-hertzbeat-1.6.0-incubating-docker-compose.tar.gz
|
||||
apache-hertzbeat-1.6.0-incubating-docker-compose.tar.gz: OK
|
||||
apache-hertzbeat-collector-1.6.0-incubating-bin.tar.gz
|
||||
apache-hertzbeat-collector-1.6.0-incubating-bin.tar.gz: OK
|
||||
apache-hertzbeat-1.7.3-src.tar.gz
|
||||
apache-hertzbeat-1.7.3-src.tar.gz: OK
|
||||
apache-hertzbeat-1.7.3-bin.tar.gz
|
||||
apache-hertzbeat-1.7.3-bin.tar.gz: OK
|
||||
apache-hertzbeat-1.7.3-docker-compose.tar.gz
|
||||
apache-hertzbeat-1.7.3-docker-compose.tar.gz: OK
|
||||
apache-hertzbeat-collector-1.7.3-bin.tar.gz
|
||||
apache-hertzbeat-collector-1.7.3-bin.tar.gz: OK
|
||||
```
|
||||
|
||||
#### 3.7 发布 Apache SVN 仓库中 dev 目录的物料包
|
||||
@@ -308,18 +312,18 @@ apache-hertzbeat-collector-1.6.0-incubating-bin.tar.gz: OK
|
||||
|
||||
```shell
|
||||
# 检出 Apache SVN 仓库中的 dev/incubator/hertzbeat 目录到 Apache HertzBeat™ 项目根目录下的 svn/dev 目录下
|
||||
svn co https://dist.apache.org/repos/dist/dev/incubator/hertzbeat svn/dev
|
||||
svn co https://dist.apache.org/repos/dist/dev/hertzbeat svn/dev
|
||||
|
||||
svn co --depth empty https://dist.apache.org/repos/dist/dev/incubator/hertzbeat
|
||||
svn co --depth empty https://dist.apache.org/repos/dist/dev/hertzbeat
|
||||
```
|
||||
|
||||
- 复制物料包到 SVN 仓库
|
||||
|
||||
创建一个版本号目录,并以${release_version}-${RC_version}的形式命名。RC_version从1开始,即候选版本从RC1开始。在发布过程中,如果出现导致投票失败的问题,需要进行修正,那么RC版本需要迭代,RC版本号需要+1。例如:为版本1.6.0-RC1投票。如果投票顺利通过,那么RC1版本的资料将作为最终版本的资料发布。如果出现问题(当hertzbeat/incubator社区进行投票时,投票者会严格检查各种发布要求和合规问题)并需要纠正,那么修正后重新启动投票,下一次的候选版本为1.6.0-RC2。
|
||||
创建一个版本号目录,并以${release_version}-${RC_version}的形式命名。RC_version从1开始,即候选版本从RC1开始。在发布过程中,如果出现导致投票失败的问题,需要进行修正,那么RC版本需要迭代,RC版本号需要+1。例如:为版本1.7.3-RC1投票。如果投票顺利通过,那么RC1版本的资料将作为最终版本的资料发布。如果出现问题并需要纠正,那么修正后重新启动投票,下一次的候选版本为1.7.3-RC2。
|
||||
|
||||
```shell
|
||||
mkdir -p svn/dev/1.6.0-RC1
|
||||
cp -f dist/* svn/dev/1.6.0-RC1
|
||||
mkdir -p svn/dev/1.7.3-RC1
|
||||
cp -f dist/* svn/dev/1.7.3-RC1
|
||||
```
|
||||
|
||||
- 提交到SVN
|
||||
@@ -331,17 +335,17 @@ cd svn/dev
|
||||
svn status
|
||||
|
||||
# 2. 添加到svn
|
||||
svn add 1.6.0-RC1
|
||||
svn add 1.7.3-RC1
|
||||
|
||||
svn status
|
||||
|
||||
# 3. 提交到svn远端服务器
|
||||
svn commit -m "release for HertzBeat 1.6.0-RC1"
|
||||
svn commit -m "release for HertzBeat 1.7.3-RC1"
|
||||
```
|
||||
|
||||
- 检查 Apache SVN 提交结果
|
||||
|
||||
> 在浏览器中访问 <https://dist.apache.org/repos/dist/dev/incubator/hertzbeat/> , 检查是否有新的版本内容
|
||||
> 在浏览器中访问 <https://dist.apache.org/repos/dist/dev/hertzbeat/> , 检查是否有新的版本内容
|
||||
|
||||
## 4. 进入社区投票阶段
|
||||
|
||||
@@ -350,24 +354,24 @@ svn commit -m "release for HertzBeat 1.6.0-RC1"
|
||||
发送社区投票邮件需要至少三个`+1`,且无`-1`。
|
||||
|
||||
> `Send to`: <dev@hertzbeat.apache.org> <br />
|
||||
> `Title`: [VOTE] Release Apache HertzBeat 1.6.0 rc1 <br />
|
||||
> `Title`: [VOTE] Release Apache HertzBeat 1.7.3 rc1 <br />
|
||||
> `Body`:
|
||||
|
||||
```text
|
||||
Hello HertzBeat Community:
|
||||
|
||||
This is a call for vote to release Apache HertzBeat version release-1.6.0-RC1.
|
||||
This is a call for vote to release Apache HertzBeat version release-1.7.3-RC1.
|
||||
|
||||
Apache HertzBeat - a real-time observability system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
|
||||
|
||||
Release notes:
|
||||
https://github.com/apache/hertzbeat/releases/tag/v1.6.0-rc1
|
||||
https://github.com/apache/hertzbeat/releases/tag/v1.7.3-rc1
|
||||
|
||||
The release candidates:
|
||||
https://dist.apache.org/repos/dist/dev/incubator/hertzbeat/1.6.0-RC1/
|
||||
https://dist.apache.org/repos/dist/dev/hertzbeat/1.7.3-RC1/
|
||||
|
||||
Git tag for the release:
|
||||
https://github.com/apache/hertzbeat/releases/tag/v1.6.0-rc1
|
||||
https://github.com/apache/hertzbeat/releases/tag/v1.7.3-rc1
|
||||
|
||||
The artifacts signed with PGP key [33545C76], corresponding to [muchunjin@apache.org], that can be found in keys file:
|
||||
https://downloads.apache.org/incubator/hertzbeat/KEYS
|
||||
@@ -391,9 +395,6 @@ release.
|
||||
[ ] No compiled archives bundled in source archive.
|
||||
[ ] Can compile from source.
|
||||
|
||||
More detail checklist please refer:
|
||||
https://cwiki.apache.org/confluence/display/INCUBATOR/Incubator+Release+Checklist
|
||||
|
||||
Steps to validate the release,Please refer to:
|
||||
https://hertzbeat.apache.org/docs/community/how_to_verify_release
|
||||
|
||||
@@ -406,13 +407,13 @@ Thanks!
|
||||
在72小时后,将统计投票结果,并发送投票结果邮件,如下所示。
|
||||
|
||||
> `Send to`: <dev@hertzbeat.apache.org> <br />
|
||||
> `Title`: [RESULT]\[VOTE\] Release Apache HertzBeat 1.6.0-rc1 <br />
|
||||
> `Title`: [RESULT]\[VOTE\] Release Apache HertzBeat 1.7.3-rc1 <br />
|
||||
> `Body`:
|
||||
|
||||
```text
|
||||
Dear HertzBeat community,
|
||||
|
||||
Thanks for your review and vote for "Release Apache HertzBeat 1.6.0-rc1"
|
||||
Thanks for your review and vote for "Release Apache HertzBeat 1.7.3-rc1"
|
||||
I'm happy to announce the vote has passed:
|
||||
---
|
||||
4 binding +1, from:
|
||||
@@ -428,7 +429,7 @@ no 0 or -1 votes.
|
||||
Vote thread:
|
||||
https://lists.apache.org/thread/t01b2lbtqzyt7j4dsbdp5qjc3gngjsdq
|
||||
---
|
||||
Thank you to everyone who helped us to verify and vote for this release. We will move to the ASF Incubator voting shortly.
|
||||
Thank you to everyone who helped us to verify and vote for this release.
|
||||
---
|
||||
Best,
|
||||
ChunJin Mu
|
||||
@@ -436,112 +437,12 @@ ChunJin Mu
|
||||
|
||||
邮件内容中的一项是`Vote thread`,在 <https://lists.apache.org/list.html?dev@hertzbeat.apache.org> 查看获取
|
||||
|
||||
### 3.2 发送孵化社区投票邮件
|
||||
|
||||
发送孵化社区投票邮件需要至少三个`+1`,且无`-1`。
|
||||
|
||||
> `Send to`: <general@incubator.apache.org> <br />
|
||||
> `Title`: [VOTE] Release Apache HertzBeat 1.6.0-rc1 <br />
|
||||
> `Body`:
|
||||
|
||||
```text
|
||||
Hello Incubator Community:
|
||||
|
||||
This is a call for a vote to release Apache HertzBeat version 1.6.0-RC1.
|
||||
The Apache HertzBeat community has voted on and approved a proposal to release Apache HertzBeat version 1.6.0-RC1.
|
||||
We now kindly request the Incubator PPMC members review and vote on this incubator release.
|
||||
Apache HertzBeat, a real-time observability system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
|
||||
|
||||
HertzBeat community vote thread:
|
||||
https://lists.apache.org/thread/t01b2lbtqzyt7j4dsbdp5qjc3gngjsdq
|
||||
|
||||
Vote result thread:
|
||||
https://lists.apache.org/thread/t5z58mvrs1drgzfyc48c9lhmd8skswn7
|
||||
|
||||
The release candidate:
|
||||
https://dist.apache.org/repos/dist/dev/incubator/hertzbeat/1.6.0-RC1/
|
||||
|
||||
Git tag for the release:
|
||||
https://github.com/apache/hertzbeat/releases/tag/v1.6.0-rc1
|
||||
|
||||
The artifacts signed with PGP key [33545C76], corresponding to [muchunjin@apache.org], that can be found in keys file:
|
||||
https://downloads.apache.org/incubator/hertzbeat/KEYS
|
||||
|
||||
The vote will be open for at least 72 hours or until the necessary number of votes are reached.
|
||||
|
||||
Please vote accordingly:
|
||||
[ ] +1 approve
|
||||
[ ] +0 no opinion
|
||||
[ ] -1 disapprove with the reason
|
||||
|
||||
More detailed checklist please refer:
|
||||
• https://cwiki.apache.org/confluence/display/INCUBATOR/Incubator+Release+Checklist
|
||||
|
||||
Steps to validate the release, Please refer to:
|
||||
• https://www.apache.org/info/verification.html
|
||||
• https://hertzbeat.apache.org/docs/community/how_to_verify_release
|
||||
---
|
||||
How to Build:
|
||||
https://hertzbeat.apache.org/docs/community/development/#build-hertzbeat-binary-package
|
||||
---
|
||||
Thanks,
|
||||
|
||||
On behalf of Apache HertzBeat community
|
||||
---
|
||||
Best,
|
||||
ChunJin Mu
|
||||
```
|
||||
|
||||
如果72小时后没有-1,回复邮件如下
|
||||
|
||||
> `Send to`: <general@incubator.apache.org> <br />
|
||||
> `Body`:
|
||||
|
||||
```text
|
||||
Thanks everyone for review and vote, 72H passed. I'll announce the vote result soon.
|
||||
|
||||
Best,
|
||||
Chunjin Mu
|
||||
```
|
||||
|
||||
然后将统计投票结果,并发送投票结果邮件,如下所示。
|
||||
|
||||
> `Send to`: <general@incubator.apache.org> <br />
|
||||
> `Title`: [RESULT]\[VOTE\] Release Apache HertzBeat 1.6.0-rc1 <br />
|
||||
> `Body`:
|
||||
|
||||
```text
|
||||
Hi Incubator Community,
|
||||
|
||||
The vote to release Apache HertzBeat 1.6.0-rc4 has passed with 3 +1 binding and no +0 or -1 votes.
|
||||
|
||||
3 binding votes, no +0 or -1 votes.
|
||||
|
||||
+3 (binding) +1, from:
|
||||
- xxx
|
||||
|
||||
no 0 or -1 votes.
|
||||
|
||||
Vote thread:
|
||||
|
||||
https://lists.apache.org/thread/m1kyn4l30y55p6q39m0ys5djvdd73h0f
|
||||
|
||||
Thanks everyone for your feedback and help with HertzBeat apache release. The HertzBeat team will take the steps to complete this release and will announce it soon.
|
||||
|
||||
Best,
|
||||
ChunJin Mu
|
||||
```
|
||||
|
||||
邮件内容中的一项是`Vote thread`,在 <https://lists.apache.org/list.html?general@incubator.apache.org> 查看获取
|
||||
|
||||
等待一天,查看看导师是否有其他意见,如果没有,发送以下公告邮件
|
||||
|
||||
## 4. 完成最终发布步骤
|
||||
|
||||
### 4.1 迁移源代码和二进制包
|
||||
|
||||
```shell
|
||||
svn mv https://dist.apache.org/repos/dist/dev/incubator/hertzbeat/1.6.0-RC1 https://dist.apache.org/repos/dist/release/incubator/hertzbeat/1.6.0 -m "transfer packages for 1.6.0-RC1"
|
||||
svn mv https://dist.apache.org/repos/dist/dev/hertzbeat/1.7.3-RC1 https://dist.apache.org/repos/dist/release/hertzbeat/1.7.3 -m "transfer packages for 1.7.3-RC1"
|
||||
```
|
||||
|
||||
### 4.2 添加新版本下载地址到官网
|
||||
@@ -555,7 +456,7 @@ svn mv https://dist.apache.org/repos/dist/dev/incubator/hertzbeat/1.6.0-RC1 http
|
||||
|
||||
### 4.3 Github 生成 Release
|
||||
|
||||
基于 release-1.6.0-rc1 分支修改创建一个名为 v1.6.0 的标签,并将此标签设置为 latest release。
|
||||
基于 release-1.7.3-rc1 分支修改创建一个名为 v1.7.3 的标签,并将此标签设置为 latest release。
|
||||
|
||||
:::tip
|
||||
在原有的 Release 上面修改,无需重新创建 Release。
|
||||
@@ -566,7 +467,7 @@ svn mv https://dist.apache.org/repos/dist/dev/incubator/hertzbeat/1.6.0-RC1 http
|
||||
- 发版标题:
|
||||
|
||||
```text
|
||||
v1.6.0
|
||||
v1.7.3
|
||||
```
|
||||
|
||||
- 描述:
|
||||
@@ -578,19 +479,19 @@ release note: xxx
|
||||
|
||||
然后点击`Publish release`按钮。
|
||||
|
||||
然后将 release-1.6.0-rc1 分支重命名 为 release-1.6.0。
|
||||
然后将 release-1.7.3-rc1 分支重命名 为 release-1.7.3。
|
||||
|
||||
### 4.4 发送新版本公告邮件
|
||||
|
||||
> `Send to`: <general@incubator.apache.org> <br />
|
||||
> `Send to`: <announce@apache.org> <br />
|
||||
> `cc`: <dev@hertzbeat.apache.org> <br />
|
||||
> `Title`: [ANNOUNCE] Apache HertzBeat 1.6.0 released <br />
|
||||
> `Title`: [ANNOUNCE] Apache HertzBeat 1.7.3 released <br />
|
||||
> `Body`:
|
||||
|
||||
```text
|
||||
Hi Community,
|
||||
|
||||
We are glad to announce the release of Apache HertzBeat 1.6.0.
|
||||
We are glad to announce the release of Apache HertzBeat 1.7.3.
|
||||
Thanks again for your help.
|
||||
|
||||
Apache HertzBeat (https://hertzbeat.apache.org/) - a real-time observability system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
|
||||
@@ -599,7 +500,7 @@ Download Link:
|
||||
https://hertzbeat.apache.org/docs/download/
|
||||
|
||||
Release Note:
|
||||
https://github.com/apache/hertzbeat/releases/tag/v1.6.0
|
||||
https://github.com/apache/hertzbeat/releases/tag/v1.7.3
|
||||
|
||||
Website:
|
||||
https://hertzbeat.apache.org/
|
||||
|
||||
@@ -9,7 +9,7 @@ sidebar_label: Download
|
||||
|
||||
:::tip
|
||||
|
||||
- 验证下载版本,请使用相应的哈希(sha512)、签名和[项目发布KEYS](https://downloads.apache.org/incubator/hertzbeat/KEYS)。
|
||||
- 验证下载版本,请使用相应的哈希(sha512)、签名和[项目发布KEYS](https://downloads.apache.org/hertzbeat/KEYS)。
|
||||
- 检查哈希和签名的方法参考 [如何验证](https://www.apache.org/dyn/closer.cgi#verify)。
|
||||
|
||||
:::
|
||||
@@ -20,9 +20,9 @@ sidebar_label: Download
|
||||
以前版本的 HertzBeat 可能会受到安全问题的影响,请考虑使用最新版本。
|
||||
:::
|
||||
|
||||
| 版本 | 日期 | 下载 | Release |
|
||||
|--------|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------|
|
||||
| v1.7.2 | 2025.07.05 | [apache-hertzbeat-1.7.2-incubating-bin.tar.gz](https://www.apache.org/dyn/closer.lua/incubator/hertzbeat/1.7.2/apache-hertzbeat-1.7.2-incubating-bin.tar.gz) (Server) ( [signature](https://downloads.apache.org/incubator/hertzbeat/1.7.2/apache-hertzbeat-1.7.2-incubating-bin.tar.gz.asc) , [sha512](https://downloads.apache.org/incubator/hertzbeat/1.7.2/apache-hertzbeat-1.7.2-incubating-bin.tar.gz.sha512) ) <br/> [apache-hertzbeat-collector-1.7.2-incubating-bin.tar.gz](https://www.apache.org/dyn/closer.lua/incubator/hertzbeat/1.7.2/apache-hertzbeat-collector-1.7.2-incubating-bin.tar.gz) (Collector) ( [signature](https://downloads.apache.org/incubator/hertzbeat/1.7.2/apache-hertzbeat-collector-1.7.2-incubating-bin.tar.gz.asc) , [sha512](https://downloads.apache.org/incubator/hertzbeat/1.7.2/apache-hertzbeat-collector-1.7.2-incubating-bin.tar.gz.sha512) ) <br/> [apache-hertzbeat-1.7.2-incubating-src.tar.gz](https://www.apache.org/dyn/closer.lua/incubator/hertzbeat/1.7.2/apache-hertzbeat-1.7.2-incubating-src.tar.gz) (Source Code) ( [signature](https://downloads.apache.org/incubator/hertzbeat/1.7.2/apache-hertzbeat-1.7.2-incubating-src.tar.gz.asc) , [sha512](https://downloads.apache.org/incubator/hertzbeat/1.7.2/apache-hertzbeat-1.7.2-incubating-src.tar.gz.sha512) ) <br/> [apache-hertzbeat-1.7.2-incubating-docker-compose.tar.gz](https://www.apache.org/dyn/closer.lua/incubator/hertzbeat/1.7.2/apache-hertzbeat-1.7.2-incubating-docker-compose.tar.gz) (Docker Compose) ( [signature](https://downloads.apache.org/incubator/hertzbeat/1.7.2/apache-hertzbeat-1.7.2-incubating-docker-compose.tar.gz.asc) , [sha512](https://downloads.apache.org/incubator/hertzbeat/1.7.2/apache-hertzbeat-1.7.2-incubating-docker-compose.tar.gz.sha512) ) | [note](https://github.com/apache/hertzbeat/releases/tag/v1.7.2) |
|
||||
| 版本 | 日期 | 下载 | Release |
|
||||
|--------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------|
|
||||
| v1.7.3 | 2025.09.05 | [apache-hertzbeat-1.7.3-bin.tar.gz](https://www.apache.org/dyn/closer.lua/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-bin.tar.gz) (Server) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-bin.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-bin.tar.gz.sha512) ) <br/> [apache-hertzbeat-collector-1.7.3-bin.tar.gz](https://www.apache.org/dyn/closer.lua/hertzbeat/1.7.3/apache-hertzbeat-collector-1.7.3-bin.tar.gz) (Collector) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-collector-1.7.3-bin.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-collector-1.7.3-bin.tar.gz.sha512) ) <br/> [apache-hertzbeat-1.7.3-src.tar.gz](https://www.apache.org/dyn/closer.lua/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-src.tar.gz) (Source Code) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-src.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-src.tar.gz.sha512) ) <br/> [apache-hertzbeat-1.7.3-docker-compose.tar.gz](https://github.com/apache/hertzbeat/releases/download/v1.7.3/apache-hertzbeat-1.7.3-docker-compose.tar.gz) (Docker Compose) ( [signature](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-docker-compose.tar.gz.asc) , [sha512](https://downloads.apache.org/hertzbeat/1.7.3/apache-hertzbeat-1.7.3-docker-compose.tar.gz.sha512) ) | [note](https://github.com/apache/hertzbeat/releases/tag/v1.7.3) |
|
||||
|
||||
## Docker 镜像版本
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
id: apollo
|
||||
title: 监控:Apollo配置中心
|
||||
sidebar_label: Apollo配置中心
|
||||
keywords: [ 开源监控系统, 开源中间件监控, Apollo配置中心监控 ]
|
||||
---
|
||||
|
||||
> 通过调用 Apollo配置中心 Prometheus 接口对 Apollo配置中心服务的通用指标进行采集监控。
|
||||
|
||||
### 前置条件
|
||||
|
||||
1. 按照[部署文档](https://www.apolloconfig.com/#/en/deployment/quick-start)搭建好Apollo配置中心。
|
||||
2. 访问```http://${someIp:somePort}/prometheus```,查看是否能访问到metrics数据。
|
||||
详情请参考:[Apollo 监控相关](https://www.apolloconfig.com/#/en/design/apollo-design?id=v-monitoring-related)
|
||||
3. 注意⚠️:从1.5.0版本开始,Apollo服务端支持通过/prometheus暴露prometheus格式的metrics
|
||||
|
||||
## 配置参数
|
||||
|
||||
| 参数名称 | 参数帮助描述 |
|
||||
|--------|------------------------------------------------------|
|
||||
| 目标Host | 被监控的对端IPV4,IPV6或域名。注意⚠️不带协议头(eg: https://, http://)。 |
|
||||
| 端口 | Pulsar的webServiceProt值,默认为8080。 |
|
||||
| 任务名称 | 标识此监控的名称,名称需要保证唯一性。 |
|
||||
| 查询超时时间 | 设置连接的超时时间,单位ms毫秒,默认3000毫秒。 |
|
||||
| 监控周期 | 监控周期性采集数据间隔时间,单位秒,可设置的最小间隔为30秒 |
|
||||
| 绑定标签 | 用于对监控资源进行分类管理 |
|
||||
| 描述备注 | 更多标识和描述此监控的备注信息,用户可以在这里备注信息 |
|
||||
|
||||
### 采集指标
|
||||
|
||||
#### 指标集合:基础信息指标
|
||||
|
||||
| 指标名称 | 指标单位 | 指标帮助描述 |
|
||||
|--------------------------------|------|---------------|
|
||||
| application_ready_time_seconds | 秒 | 应用从启动到就绪状态的耗时 |
|
||||
| process_uptime_seconds | 秒 | 进程已运行的总时间 |
|
||||
| process_cpu_usage | % | 当前进程的CPU使用率 |
|
||||
|
||||
#### 指标集合:jvm
|
||||
|
||||
| 指标名称 | 指标单位 | 指标帮助描述 |
|
||||
|-----------------------------------|------|------------------|
|
||||
| system_cpu_usage | % | 系统CPU使用率 |
|
||||
| system_load_average_1m | 无 | 系统最近一分钟平均负载 |
|
||||
| jvm_memory_committed_bytes | MB | JVM已向操作系统申请的内存大小 |
|
||||
| jvm_memory_used_bytes | MB | JVM当前实际使用的内存大小 |
|
||||
| jvm_memory_max_bytes | MB | JVM可使用的最大内存限制 |
|
||||
| jvm_gc_pause_seconds_count | 无 | JVM GC暂停事件的总次数 |
|
||||
| jvm_gc_pause_seconds_sum | 无 | JVM GC暂停的总耗时 |
|
||||
| jvm_memory_usage_after_gc_percent | 无 | JVM GC后的内存使用率 |
|
||||
|
||||
#### 指标集合:系统资源
|
||||
|
||||
| 指标名称 | 指标单位 | 指标帮助描述 |
|
||||
|--------------------------|------|--------------------|
|
||||
| process_files_max_files | 无 | 允许进程打开的最大文件描述符数量限制 |
|
||||
| process_files_open_files | 无 | 进程当前已打开的文件描述符数量 |
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
id: idrac
|
||||
title: Dell iDRAC 监控
|
||||
sidebar_label: 服务器监控
|
||||
keywords: [开源监控工具, 开源服务器监控, Dell iDRAC 监控]
|
||||
---
|
||||
|
||||
> 使用 SNMP 协议收集和监控 Dell 服务器的通用性能指标。
|
||||
|
||||
**协议: SNMP**
|
||||
|
||||
## 监控前准备
|
||||
|
||||
1. 目标 Dell 服务器支持 **SNMP 协议**。
|
||||
2. **iDRAC** (Integrated Dell Remote Access Controller) 已配置网络接口,允许访问 **SNMP 端口**。
|
||||
3. 已配置 **SNMP 团体字** 或 **用户凭证** 并分配了适当的 **权限**。
|
||||
|
||||
这些是您可以遵循的基本检查项,有关在 Dell iDRAC 上启用和配置 SNMP 的详细信息,您可以查阅 Dell 服务器的特定用户手册。
|
||||
|
||||
## 配置参数
|
||||
|
||||
| 参数名称 | 参数说明 |
|
||||
| -------- |---------------------------------------------------------------------------------------------------|
|
||||
| 目标Host | 被监控设备的 IPv4、IPv6 或域名。注意:不带协议头 (例如 https://, http://)。 |
|
||||
| 端口 | 服务器 SNMP 端口号,默认为 161。 |
|
||||
| SNMP 版本| 选择 SNMPv1、SNMPv2c 或 SNMPv3 |
|
||||
| SNMP 团体字| SNMP v1 v2c 的团体字 |
|
||||
| 用户名 | SNMP v3 的用户名 |
|
||||
| Context Name| SNMP v3 的上下文名称 |
|
||||
| 认证密码 | SNMP v3 的认证密码 |
|
||||
| 认证加密 | SNMP v3 的认证加密方式 (MD5 或 SHA1) |
|
||||
| 隐私密码 | SNMP v3 的隐私密码 |
|
||||
| 隐私加密 | SNMP v3 的隐私加密方式 (DES 或 AES128) |
|
||||
|
||||
### 采集的指标
|
||||
|
||||
#### 指标集: 系统状态
|
||||
|
||||
| 指标名称 | 单位 | 指标说明 |
|
||||
| -------------------- | ---- | -------------------------------------------------- |
|
||||
| 全局系统状态 | 无 | 系统的整体状态 |
|
||||
| 系统前面板状态 | 无 | 系统 LCD 的状态 |
|
||||
| 全局存储状态 | 无 | 存储子系统的整体状态 |
|
||||
| 系统电源状态 | 无 | 系统的当前电源状态 |
|
||||
| 系统运行时间 | 天 | 自系统上次开机以来的时间 |
|
||||
|
||||
#### 指标集: 电源供应器
|
||||
|
||||
| 指标名称 | 单位 | 指标说明 |
|
||||
| ----------- | ---- | -------------------------------------------------- |
|
||||
| 索引 | 无 | 电源供应器的索引 |
|
||||
| 名称 | 无 | 电源供应器的名称/位置 |
|
||||
| 功率 | W | 电源供应器的输出功率 |
|
||||
| 类型 | 无 | 电源供应器的类型 (线性、开关、电池等) |
|
||||
| 状态 | 无 | 电源供应器的当前状态 |
|
||||
|
||||
#### 指标集: 冷却设备
|
||||
|
||||
| 指标名称 | 单位 | 指标说明 |
|
||||
| ----------- | ---- | -------------------------------------------------- |
|
||||
| 索引 | 无 | 冷却设备的索引 |
|
||||
| 名称 | 无 | 冷却设备的名称/位置 |
|
||||
| 类型 | 无 | 冷却设备的类型 (风扇、鼓风机、热管等) |
|
||||
| 转速 | RPM | 当前每分钟转速读数 |
|
||||
| 状态 | 无 | 冷却设备的当前状态 |
|
||||
|
||||
#### 指标集: 温度传感器
|
||||
|
||||
| 指标名称 | 单位 | 指标说明 |
|
||||
| ----------- | ---- | -------------------------------------------------- |
|
||||
| 索引 | 无 | 温度传感器的索引 |
|
||||
| 名称 | 无 | 温度传感器的名称/位置 |
|
||||
| 温度 | °C | 当前温度读数 |
|
||||
| 状态 | 无 | 温度传感器的当前状态 |
|
||||
|
||||
#### 指标集: 电压传感器
|
||||
|
||||
| 指标名称 | 单位 | 指标说明 |
|
||||
| ----------- | ---- | -------------------------------------------------- |
|
||||
| 索引 | 无 | 电压传感器的索引 |
|
||||
| 名称 | 无 | 电压传感器的名称/位置 |
|
||||
| 电压 | V | 当前电压读数 |
|
||||
| 类型 | 无 | 电压类型 (3.3V, 5V, 12V 等) |
|
||||
| 状态 | 无 | 电压传感器的当前状态 |
|
||||
|
||||
#### 指标集: 内存设备
|
||||
|
||||
| 指标名称 | 单位 | 指标说明 |
|
||||
| ----------- | ---- | -------------------------------------------------- |
|
||||
| 索引 | 无 | 内存设备的索引 |
|
||||
| 名称 | 无 | 内存设备的名称/位置 |
|
||||
| 类型 | 无 | 内存类型 (DDR, DDR2, DDR3, DDR4 等) |
|
||||
| 容量 | G | 内存设备的容量 |
|
||||
| 状态 | 无 | 内存设备的当前状态 |
|
||||
|
||||
#### 指标集: 处理器
|
||||
|
||||
| 指标名称 | 单位 | 指标说明 |
|
||||
| ----------- | ---- | -------------------------------------------------- |
|
||||
| 索引 | 无 | 处理器的索引 |
|
||||
| 名称 | 无 | 处理器的名称/位置 |
|
||||
| 频率 | MHz | 处理器的当前频率 |
|
||||
| 家族 | 无 | 处理器家族 (Pentium, Xeon, Core i7 等) |
|
||||
| 状态 | 无 | 处理器的当前状态 |
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user