Compare commits

...
Author SHA1 Message Date
tomsun28 09103ae89c [docs] update dockerfile
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-09-20 19:04:30 +08:00
5cdfe545b5 [doc] add japanese i18n in app-idrac.yml (#3766)
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-20 00:48:03 +08:00
+16 9be7295084 [fix]bugfix:Prevent Long.parseLong() error when search param is a float (#3483)
Signed-off-by: Sarthak Arora <f20200060@pilani.bits-pilani.ac.in>
Signed-off-by: Cyanty <153884653+Cyanty@users.noreply.github.com>
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Signed-off-by: Wenqi Luo <495872803@qq.com>
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
Signed-off-by: lynx009 <2030509072@qq.com>
Signed-off-by: Tom <tomsun28@outlook.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: 淞筱 <2030509072@qq.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: Logic <zqr10159@dromara.org>
Co-authored-by: Sarthak Arora <f20200060@pilani.bits-pilani.ac.in>
Co-authored-by: Jast <shenghang@apache.org>
Co-authored-by: DeleiGuo <deleiguo@163.com>
Co-authored-by: Cyanty <153884653+Cyanty@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: Duansg <siguoduan@gmail.com>
Co-authored-by: liutianyou <tianyou.liu@qq.com>
Co-authored-by: VampireAchao <achao@apache.org>
Co-authored-by: Wenqi Luo <495872803@qq.com>
Co-authored-by: Yang Chen <1597081640@qq.com>
Co-authored-by: tuzuy <95274591+tuzuy@users.noreply.github.com>
Co-authored-by: tuzy <tuzy.cq@chinaccs.cn>
Co-authored-by: Kerwin Bryant <kerwin612@qq.com>
Co-authored-by: liutianyou <liutianyou@apache.org>
Co-authored-by: carl pinto <carlpinto25@gmail.com>
Co-authored-by: TJxiaobao <2922035952@qq.com>
Co-authored-by: cxhello <49056040+cxhello@users.noreply.github.com>
2025-09-20 00:24:51 +08:00
6905bcc986 [feat] Support monitoring center indicator favorites feature (#3735)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-19 23:53:26 +08:00
Duansg 1050032a13 [improve] SSE exception handling improvements (#3775) 2025-09-19 00:24:49 +08:00
DeleiGuo 9b223dd217 [doc] replace with the new ASF logo (#3770) 2025-09-17 01:16:25 +08:00
Duansg 0a1b5961e9 [feat] Support apollo configuration center monitoring (#3768) 2025-09-15 08:48:52 +08:00
tuzuyandtuzy a8a70b76d7 [feat] Add configuration file for Dell iDRAC monitoring type (#3763)
Co-authored-by: tuzy <tuzy.cq@chinaccs.cn>
2025-09-13 00:08:46 +08:00
+1 05b63903b5 [GSOC] Added all the necessary tools across services (#3722)
Signed-off-by: Sarthak Arora <f20200060@pilani.bits-pilani.ac.in>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Jast <shenghang@apache.org>
Co-authored-by: Duansg <siguoduan@gmail.com>
Co-authored-by: DeleiGuo <deleiguo@163.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Logic <zqr10159@dromara.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: lynx009 <2030509072@qq.com>
2025-09-10 08:31:05 +08:00
Calvin 7d8b55dfb8 [docs] Update contributors (#3759) 2025-09-09 08:10:40 +08:00
DuansgandTom 72a3e59ca6 [improve] Prometheus streaming parsing optimization (#3752)
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-07 23:20:23 +08:00
3ab94a751b [Improve] optimize the dashboard page and adapt to the new theme (#3730)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-07 23:00:12 +08:00
66049f189b [OSPP] Add RISC-V related help document. (#3712)
Signed-off-by: lynx009 <2030509072@qq.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-07 22:45:21 +08:00
DeleiGuoandTom 69ec3f8751 [bugfix] synchronise AI-related configuration for Docker Compose (#3751)
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-07 22:42:43 +08:00
feb43c5678 [GSOC] Chat UI, Conversation Management, OpenAI Chat Client Support, More monitoring tools. (#3679)
Signed-off-by: Sarthak Arora <f20200060@pilani.bits-pilani.ac.in>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Jast <shenghang@apache.org>
Co-authored-by: Duansg <siguoduan@gmail.com>
Co-authored-by: DeleiGuo <deleiguo@163.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Logic <zqr10159@dromara.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-07 22:36:04 +08:00
liutianyou 1fae67282d [doc] update doc for how to release and add release note for 1.7.3 (#3749) 2025-09-06 21:00:04 +08:00
122 changed files with 12692 additions and 1632 deletions
+82 -4
View File
@@ -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,
+8
View File
@@ -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>
@@ -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);
}
@@ -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();
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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;
}
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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.
""";
}
@@ -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"));
}
}
}
@@ -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 {
}
@@ -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);
}
@@ -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 {
}
@@ -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 contentformat 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;
}
@@ -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);
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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;
}
}
}
@@ -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);
}
}
}
}
@@ -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;
}
}
@@ -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.
@@ -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();
}
}
@@ -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);
}
@@ -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();
}
@@ -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();
}
@@ -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);
}
@@ -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());
}
}
}
@@ -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();
}
}
}
@@ -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();
}
}
}
@@ -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();
}
}
}
@@ -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;
}
}
@@ -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);
}
@@ -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");
}
}
@@ -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());
@@ -223,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);
}
@@ -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 = """
@@ -287,4 +297,166 @@ class OnlineParserTest {
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());
}
}
@@ -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;
}
@@ -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());
}
}
@@ -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));
@@ -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;
}
}
}
@@ -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);
}
@@ -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;
}
@@ -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;
@@ -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);
}
@@ -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);
}
}
@@ -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);
@@ -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);
}
}
@@ -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 categoryservice-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.
@@ -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());
}
}
}
@@ -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();
}
}
@@ -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
@@ -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);
}
}
+21 -21
View File
@@ -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
+253
View File
@@ -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>
+2 -4
View File
@@ -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:
![cert](/img/docs/hertzbeat-cert.png)
### 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)
+64 -163
View File
@@ -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 releasePlease 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/
+1 -1
View File
@@ -22,7 +22,7 @@ Previous releases of HertzBeat may be affected by security issues, please use th
| 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://www.apache.org/dyn/closer.lua/hertzbeat/1.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) |
| 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
+371
View File
@@ -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
---
+61
View File
@@ -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 |
+104
View File
@@ -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 |
+25 -2
View File
@@ -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
+96
View File
@@ -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.
+14 -13
View File
@@ -23,10 +23,10 @@ QuestDB is an open-source time-series database that stands out in the field of t
> 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:
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)
@@ -37,9 +37,9 @@ 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:
For Windows users:
Download the zip package and extract it to C:\questdb or a custom directory.
Download the zip package and extract it to C:\questdb or a custom directory.
2. Start QuestDB
@@ -55,9 +55,9 @@ questdb.exe start
3. Set up access password
QuestDB enables authentication through a configuration file, which needs to be modified manually.
QuestDB enables authentication through a configuration file, which needs to be modified manually.
Edit the configuration file:
Edit the configuration file:
```shell
# For Linux/macOS
@@ -67,7 +67,7 @@ vi /opt/questdb/conf/server.conf
notepad C:\questdb\conf\server.conf
```
Enable authentication and configure the password:
Enable authentication and configure the password:
```shell
# Enable authentication (disabled by default)
@@ -82,7 +82,7 @@ http.security.admin.password=YourStrongPassword123!
http.bind.to=127.0.0.1:9000
```
Restart QuestDB for the changes to take effect:
Restart QuestDB for the changes to take effect:
```shell
# For Linux/macOS
@@ -96,13 +96,13 @@ questdb.exe start
4. Configure QuestDB connection in HertzBeat's application.yml file
Modify HertzBeat's configuration file
Modify HertzBeat's configuration file
Locate and edit the configuration file at hertzbeat/config/application.yml
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.
⚠️ 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.**
**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:
@@ -133,4 +133,5 @@ Parameter Description:
cairo.default.ttl=30d
```
5. Restart HertzBeat
5. Restart HertzBeat
<!-- markdownlint-enable MD029 -->
+5 -10
View File
@@ -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` 的证书是实体的。
证书样例:
@@ -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 releasePlease 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/
@@ -22,7 +22,7 @@ sidebar_label: 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://www.apache.org/dyn/closer.lua/hertzbeat/1.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) |
| 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 等) |
| 状态 | 无 | 处理器的当前状态 |
@@ -0,0 +1,93 @@
---
id: risc-v
title: HertzBeat 在 RISC-V 架构环境中运行
sidebar_label: RISC-V
keywords: [ 开源监控系统, RISC-V架构, RISC-V 运行 HertzBeat ]
---
> 如何搭建 RISC-V 环境,并使 HertzBeat 在 RISC-V 架构环境中运行。
## RISC-V 环境搭建
确保你的 RISC-V 开发环境已正确配置,包括拥有兼容 RISC-V 的设备或模拟器。这里采用在 Ubuntu 环境上搭建 QEMU 模拟器的方式,来模拟 RISC-V 架构。
**1. 安装 QEMU 以及软件包**
```shell
sudo apt update
sudo apt install opensbi qemu-system-misc u-boot-qemu
```
**2. 下载 RISC-V 架构的 Ubuntu 镜像**
下载地址:```https://ubuntu.com/download/risc-v```
选择 `QEMU模拟器` 下载需要的版本。
** 3. 在 QEMU 上安装 Ubuntu
> 使用 QEMU 启动 Ubuntu,模拟 RISC-V 架构。一些参数,如启动、设置磁盘大小等可以灵活设置。
```shell
# 解压镜像
xz -dk ubuntu-24.04.2-preinstalled-server-riscv64.img.xz
# 也可自定义扩大磁盘
qemu-img resize -f raw ubuntu-24.04-preinstalled-server-riscv64.img +5G
# 使用 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
```
等待控制台输出完毕后,使用用户 Ubuntu 和默认密码 Ubuntu 登录;系统会要求您选择一个新密码。详细可参考:[Ubuntu官网文档](https://canonical-ubuntu-boards.readthedocs-hosted.com/en/latest/how-to/qemu-riscv/)
## 安装并配置支持 RISC-V 架构的 JDK
> 在启动的镜像中配置支持 RISC-V 架构的 JDK,这里选用 Temurin JDK 17。
**1. 下载 Temurin JDK**
下载地址:```https://adoptium.net/zh-CN/temurin/releases```
**2. 配置 JDK 环境**
> 根据下载的版本不同,做适当更改
```shell
# 安装工具
sudo apt update
sudo apt install -y tar wget
# 创建安装目录
sudo mkdir -p /usr/lib/jvm
# 解压到系统目录
sudo tar -xzf OpenJDK17U-jdk_riscv64_linux_hotspot_17.0.15_6.tar.gz -C /usr/lib/jvm
# 编辑环境变量,添加以下内容
sudo nano /etc/profile.d/java.sh
export JAVA_HOME=/usr/lib/jvm/jdk-17.0.15+6
export PATH=$JAVA_HOME/bin:$PATH
# 使配置生效
source /etc/profile.d/java.sh
# 验证安装
java -version
```
## 运行 HertzBeat
> 在 [Hertzbeat官网](https://hertzbeat.apache.org/zh-cn/docs/download/) 下载安装包,并按照官网部署教程运行即可,以 1.7.2 版本为例。
```shell
cd apache-hertzbeat-1.7.2-incubating-bin/bin/
./startup.sh
```
> 注意事项:
>
> 1. 请将 `OpenJDK17U-jdk_riscv64_linux_hotspot_17.0.15_6.tar.gz` 替换为您实际下载的 JDK 文件名。
> 2. 确保模拟器具备网络访问能力,以下载 HertzBeat。
> 3. 若遇到问题,请检查磁盘权限和 Java 环境路径配置。
@@ -87,8 +87,6 @@ QuestDB 是一款开源的时序数据库,因其高性能和低延迟等特点
questdb.exe start
```
4. 在hertzbeat的`application.yml`配置文件配置QuestDB数据库连接
配置HertzBeat的配置文件
@@ -111,8 +109,6 @@ QuestDB 是一款开源的时序数据库,因其高性能和低延迟等特点
password: quest
```
参数说明:
| 参数名称 | 参数说明 |
@@ -129,4 +125,3 @@ QuestDB 是一款开源的时序数据库,因其高性能和低延迟等特点
> ```
5. 重启 HertzBeat
+4 -1
View File
@@ -199,7 +199,8 @@
"help/hikvision_isapi",
"help/dahua",
"help/uniview",
"help/synology_nas"
"help/synology_nas",
"help/idrac"
]
},
{
@@ -279,7 +280,9 @@
"help/grafana_dashboard",
"help/mcp_sse_server",
"help/collector",
"help/ai_agent_chat",
"help/ai_config",
"help/risc-v",
"help/issue"
]
},
+43 -210
View File
@@ -1,210 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 7127.6 2890" enable-background="new 0 0 7127.6 2890" xml:space="preserve">
<path fill="#6D6E71" d="M7104.7,847.8c15.3,15.3,22.9,33.7,22.9,55.2c0,21.5-7.6,39.9-22.9,55.4c-15.3,15.4-33.8,23.1-55.6,23.1
c-21.8,0-40.2-7.6-55.4-22.9c-15.1-15.3-22.7-33.7-22.7-55.2c0-21.5,7.6-39.9,22.9-55.4c15.3-15.4,33.7-23.1,55.4-23.1
C7070.9,824.9,7089.4,832.5,7104.7,847.8z M7098.1,951.9c13.3-13.6,20-29.8,20-48.7s-6.6-35-19.8-48.5
c-13.2-13.4-29.4-20.1-48.6-20.1c-19.2,0-35.4,6.7-48.7,20.2c-13.3,13.5-19.9,29.7-19.9,48.7c0,19,6.6,35.2,19.7,48.6
c13.1,13.4,29.3,20.1,48.5,20.1S7084.7,965.4,7098.1,951.9z M7087.1,888.1c0,14-6.1,22.8-18.4,26.4l22.5,30.5h-18.2l-20.3-28.3
h-18.6v28.3h-14.7v-84.6h31.8c12.8,0,22,2.2,27.6,6.6C7084.4,871.4,7087.1,878.4,7087.1,888.1z M7068.2,900c3-2.4,4.4-6.5,4.4-12
c0-5.5-1.5-9.4-4.5-11.6c-3-2.2-8.4-3.2-16-3.2h-18v30.5h17.5C7059.7,903.6,7065.3,902.4,7068.2,900z"/>
<path fill="#6D6E71" d="M1803.6,499.8v155.4h-20V499.8h-56.8v-19.2h133.9v19.2H1803.6z"/>
<path fill="#6D6E71" d="M2082.2,655.2v-76.9h-105.2v76.9h-20V480.5h20v78.9h105.2v-78.9h20v174.7H2082.2z"/>
<path fill="#6D6E71" d="M2241.4,499.8v57.4h88.1v19.2h-88.1v59.8h101.8v19h-121.8V480.5H2340v19.2H2241.4z"/>
<path fill="#D22128" d="M1574.5,1852.4l417.3-997.6h80.1l417.3,997.6h-105.4l-129.3-311.9h-448.2l-127.9,311.9H1574.5z M2032.6,970
l-205.1,493.2h404.7L2032.6,970z"/>
<path fill="#D22128" d="M2596.9,1852.4V854.8H3010c171.4,0,295.1,158.8,295.1,313.3c0,163-115.2,316.1-286.6,316.1h-324.6v368.1
H2596.9z M2693.9,1397.1h318.9c118,0,193.9-108.2,193.9-229c0-125.1-92.7-226.2-202.3-226.2h-310.5V1397.1z"/>
<path fill="#D22128" d="M3250.5,1852.4l417.3-997.6h80.1l417.3,997.6h-105.4l-129.3-311.9h-448.2l-127.9,311.9H3250.5z M3708.6,970
l-205.1,493.2h404.7L3708.6,970z"/>
<path fill="#D22128" d="M4637.3,849.1c177,0,306.3,89.9,368.1,217.8l-78.7,47.8c-63.2-132.1-186.9-177-295.1-177
c-238.9,0-369.5,213.6-369.5,414.5c0,220.6,161.6,420.1,373.7,420.1c112.4,0,244.5-56.2,307.7-185.5l81.5,42.1
c-64.6,148.9-241.7,231.8-394.8,231.8c-274,0-466.5-261.3-466.5-514.2C4163.8,1106.3,4336.6,849.1,4637.3,849.1z"/>
<path fill="#D22128" d="M5949.1,854.8v997.6h-98.4v-466.5h-591.5v466.5h-96.9V854.8h96.9v444h591.5v-444H5949.1z"/>
<path fill="#D22128" d="M6844.6,1765.2v87.1h-670.2V854.8H6832v87.1h-560.6v359.7h489v82.9h-489v380.8H6844.6z"/>
<path fill="#6D6E71" d="M1667.6,2063.6c11.8,3.5,22.2,8.3,31,14.2l-10.3,22.6c-9-6-18.6-10.4-28.9-13.4c-10.2-2.9-20-4.4-29.2-4.4
c-13.6,0-24.5,2.4-32.6,7.3c-8.1,4.9-12.2,11.8-12.2,20.7c0,7.6,2.3,14,6.8,19c4.5,5,10.2,8.9,17,11.7c6.8,2.8,16.1,6,28,9.6
c14.4,4.6,26,8.9,34.7,12.9c8.8,4,16.3,9.9,22.5,17.8c6.2,7.8,9.3,18.2,9.3,31c0,11.7-3.2,21.8-9.5,30.6
c-6.3,8.7-15.3,15.5-26.8,20.3c-11.6,4.8-24.9,7.2-40,7.2c-15.1,0-29.7-2.9-43.9-8.7c-14.2-5.8-26.4-13.6-36.6-23.4l10.7-21.6
c9.6,9.4,20.7,16.7,33.3,21.9c12.6,5.2,24.8,7.8,36.8,7.8c15.3,0,27.3-3,36.1-8.9c8.8-5.9,13.2-13.9,13.2-23.9
c0-7.8-2.3-14.3-6.9-19.4c-4.6-5.1-10.3-9-17.1-11.9c-6.8-2.8-16.1-6-28-9.6c-14.2-4.2-25.7-8.3-34.6-12.2
c-8.9-3.9-16.4-9.7-22.5-17.5c-6.1-7.7-9.2-17.9-9.2-30.6c0-10.9,3-20.4,9-28.6c6-8.2,14.6-14.6,25.6-19.1
c11.1-4.5,23.8-6.8,38.2-6.8C1643.8,2058.3,1655.7,2060.1,1667.6,2063.6z"/>
<path fill="#6D6E71" d="M1980.1,2072.8c16.8,9.4,30.2,22.3,40,38.4c9.8,16.2,14.8,33.9,14.8,53.3c0,19.5-4.9,37.4-14.8,53.6
c-9.8,16.3-23.2,29.1-40,38.6c-16.8,9.5-35.3,14.3-55.2,14.3c-20.3,0-38.8-4.7-55.7-14.3c-16.8-9.5-30.2-22.4-40-38.6
c-9.8-16.3-14.8-34.1-14.8-53.6c0-19.5,4.9-37.3,14.8-53.5c9.8-16.2,23.2-29,40-38.3c16.8-9.4,35.4-14,55.7-14
C1944.8,2058.6,1963.2,2063.3,1980.1,2072.8z M1881.9,2092.7c-13.1,7.4-23.6,17.5-31.4,30.1c-7.8,12.6-11.8,26.5-11.8,41.7
c0,15.3,3.9,29.3,11.8,42c7.8,12.7,18.3,22.8,31.4,30.2c13.1,7.4,27.4,11.1,42.9,11.1c15.5,0,29.7-3.7,42.7-11.1
c13-7.4,23.3-17.4,31.1-30.2c7.7-12.7,11.6-26.7,11.6-42s-3.9-29.2-11.6-41.8c-7.7-12.6-18.1-22.6-31.1-30
c-13-7.4-27.2-11.2-42.6-11.2C1909.4,2081.5,1895.1,2085.2,1881.9,2092.7z"/>
<path fill="#6D6E71" d="M2186.5,2082.4v74h98.4v23.2h-98.4v90.2h-24.1v-210.6h133.8v23.2H2186.5z"/>
<path fill="#6D6E71" d="M2491.6,2082.4v187.4h-24.1v-187.4h-68.4v-23.2h161.4v23.2H2491.6z"/>
<path fill="#6D6E71" d="M2871.8,2269.8l-56.8-177.4l-57.6,177.4h-24.5l-70.5-210.6h25.9l57.9,182.7l57.1-182.4l24.1-0.3l57.7,182.7
l57.1-182.7h25l-70.6,210.6H2871.8z"/>
<path fill="#6D6E71" d="M3087.3,2216.6l-23.5,53.2h-25.6l94.4-210.6h25l94.1,210.6h-26.1l-23.5-53.2H3087.3z M3144.5,2086.6
l-46.9,106.8h94.4L3144.5,2086.6z"/>
<path fill="#6D6E71" d="M3461.1,2202.7c-6,0.4-10.7,0.6-14.1,0.6h-56v66.5H3367v-210.6h80c26.2,0,46.6,6.2,61.2,18.5
c14.5,12.3,21.8,29.8,21.8,52.3c0,17.2-4.1,31.7-12.2,43.3c-8.1,11.6-19.8,20-35,25l49.2,71.5h-27.3L3461.1,2202.7z M3491.3,2167.6
c10.3-8.4,15.5-20.8,15.5-37c0-15.9-5.2-27.9-15.5-36c-10.3-8.1-25.1-12.2-44.3-12.2h-56v97.8h56
C3466.2,2180.2,3481,2176,3491.3,2167.6z"/>
<path fill="#6D6E71" d="M3688.3,2082.4v69.2h106.2v23.2h-106.2v72.1h122.8v22.9h-146.9v-210.6h142.9v23.2H3688.3z"/>
<path fill="#6D6E71" d="M4147,2082.4v74h98.4v23.2H4147v90.2h-24.1v-210.6h133.8v23.2H4147z"/>
<path fill="#6D6E71" d="M4523.3,2072.8c16.8,9.4,30.2,22.3,40,38.4c9.8,16.2,14.8,33.9,14.8,53.3c0,19.5-4.9,37.4-14.8,53.6
c-9.8,16.3-23.2,29.1-40,38.6c-16.8,9.5-35.3,14.3-55.2,14.3c-20.3,0-38.8-4.7-55.7-14.3c-16.8-9.5-30.2-22.4-40-38.6
c-9.8-16.3-14.8-34.1-14.8-53.6c0-19.5,4.9-37.3,14.8-53.5c9.8-16.2,23.2-29,40-38.3c16.8-9.4,35.4-14,55.7-14
C4488.1,2058.6,4506.5,2063.3,4523.3,2072.8z M4425.2,2092.7c-13.1,7.4-23.6,17.5-31.4,30.1c-7.8,12.6-11.8,26.5-11.8,41.7
c0,15.3,3.9,29.3,11.8,42c7.8,12.7,18.3,22.8,31.4,30.2c13.1,7.4,27.4,11.1,42.9,11.1c15.5,0,29.7-3.7,42.7-11.1
c13-7.4,23.3-17.4,31.1-30.2c7.7-12.7,11.6-26.7,11.6-42s-3.9-29.2-11.6-41.8c-7.7-12.6-18.1-22.6-31.1-30
c-13-7.4-27.2-11.2-42.6-11.2C4452.6,2081.5,4438.3,2085.2,4425.2,2092.7z"/>
<path fill="#6D6E71" d="M4854.7,2247.7c-15.7,15.5-37.3,23.3-64.8,23.3c-27.7,0-49.4-7.8-65.1-23.3c-15.7-15.5-23.6-37-23.6-64.6
v-124h24.1v124c0,20.3,5.8,36.1,17.3,47.5c11.6,11.4,27.3,17.1,47.3,17.1c20.1,0,35.8-5.7,47.1-17c11.4-11.3,17-27.2,17-47.7v-124
h24.1v124C4878.2,2210.7,4870.4,2232.2,4854.7,2247.7z"/>
<path fill="#6D6E71" d="M5169.5,2269.8l-126.3-169.1v169.1h-24.1v-210.6h25l126.3,169.3v-169.3h23.8v210.6H5169.5z"/>
<path fill="#6D6E71" d="M5478.4,2073.1c16.4,9.3,29.4,21.9,38.9,37.9c9.6,16,14.3,33.9,14.3,53.5s-4.8,37.6-14.3,53.6
c-9.5,16.1-22.6,28.7-39.3,37.9c-16.6,9.2-35.2,13.8-55.5,13.8h-84.3v-210.6h85.2C5443.7,2059.2,5462,2063.8,5478.4,2073.1z
M5362.3,2246.9h61.4c15.5,0,29.6-3.5,42.3-10.6c12.7-7.1,22.8-16.9,30.2-29.5c7.4-12.5,11.1-26.5,11.1-42
c0-15.5-3.8-29.4-11.3-41.9c-7.5-12.5-17.7-22.3-30.6-29.6c-12.8-7.2-27-10.9-42.6-10.9h-60.5V2246.9z"/>
<path fill="#6D6E71" d="M5668.6,2216.6l-23.5,53.2h-25.6l94.4-210.6h25l94.1,210.6H5807l-23.5-53.2H5668.6z M5725.8,2086.6
l-46.9,106.8h94.4L5725.8,2086.6z"/>
<path fill="#6D6E71" d="M5991,2082.4v187.4H5967v-187.4h-68.4v-23.2h161.4v23.2H5991z"/>
<path fill="#6D6E71" d="M6175.9,2269.8v-210.6h24.1v210.6H6175.9z"/>
<path fill="#6D6E71" d="M6493.7,2072.8c16.8,9.4,30.2,22.3,40,38.4c9.8,16.2,14.8,33.9,14.8,53.3c0,19.5-4.9,37.4-14.8,53.6
c-9.8,16.3-23.2,29.1-40,38.6c-16.8,9.5-35.3,14.3-55.2,14.3c-20.3,0-38.8-4.7-55.7-14.3c-16.8-9.5-30.2-22.4-40-38.6
c-9.8-16.3-14.8-34.1-14.8-53.6c0-19.5,4.9-37.3,14.8-53.5c9.8-16.2,23.2-29,40-38.3c16.8-9.4,35.4-14,55.7-14
C6458.5,2058.6,6476.9,2063.3,6493.7,2072.8z M6395.6,2092.7c-13.1,7.4-23.6,17.5-31.4,30.1c-7.8,12.6-11.8,26.5-11.8,41.7
c0,15.3,3.9,29.3,11.8,42c7.8,12.7,18.3,22.8,31.4,30.2c13.1,7.4,27.4,11.1,42.9,11.1c15.5,0,29.7-3.7,42.7-11.1
c13-7.4,23.3-17.4,31.1-30.2c7.7-12.7,11.6-26.7,11.6-42s-3.9-29.2-11.6-41.8c-7.7-12.6-18.1-22.6-31.1-30
c-13-7.4-27.2-11.2-42.6-11.2C6423,2081.5,6408.8,2085.2,6395.6,2092.7z"/>
<path fill="#6D6E71" d="M6826.5,2269.8l-126.3-169.1v169.1h-24.1v-210.6h25l126.3,169.3v-169.3h23.8v210.6H6826.5z"/>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-4516.6152" y1="-2338.7222" x2="-4108.4111" y2="-1861.3982" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 5117.8774 -2859.9343)">
<stop offset="0" style="stop-color:#F69923"/>
<stop offset="0.3123" style="stop-color:#F79A23"/>
<stop offset="0.8383" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_1_)" d="M1230.1,13.7c-45.3,26.8-120.6,102.5-210.5,212.3l82.6,155.9c58-82.9,116.9-157.5,176.3-221.2
c4.6-5.1,7-7.5,7-7.5c-2.3,2.5-4.6,5-7,7.5c-19.2,21.2-77.5,89.2-165.5,224.4c84.7-4.2,214.9-21.6,321.1-39.7
c31.6-177-31-258-31-258S1323.4-41.4,1230.1,13.7z"/>
<path fill="none" d="M1090.2,903.1c0.6-0.1,1.2-0.2,1.8-0.3l-11.9,1.3c-0.7,0.3-1.4,0.7-2.1,1
C1082.1,904.4,1086.2,903.7,1090.2,903.1z"/>
<path fill="none" d="M1005.9,1182.3c-6.7,1.5-13.7,2.7-20.7,3.7C992.3,1185,999.2,1183.8,1005.9,1182.3z"/>
<path fill="none" d="M432.9,1808.8c0.9-2.3,1.8-4.7,2.6-7c18.2-48,36.2-94.7,54-140.1c20-51,39.8-100.4,59.3-148.3
c20.6-50.4,40.9-99.2,60.9-146.3c21-49.4,41.7-97,62-142.8c16.5-37.3,32.8-73.4,48.9-108.3c5.4-11.7,10.7-23.2,16-34.6
c10.5-22.7,21-44.8,31.3-66.5c9.5-20,19-39.6,28.3-58.8c3.1-6.4,6.2-12.8,9.3-19.1c0.5-1,1-2,1.5-3.1l-10.2,1.1l-8-15.9
c-0.8,1.6-1.6,3.1-2.4,4.6c-14.5,28.8-28.9,57.9-43.1,87.2c-8.2,16.9-16.4,34-24.6,51c-22.6,47.4-44.8,95.2-66.6,143.3
c-22.1,48.6-43.7,97.5-64.9,146.5c-20.8,48.1-41.3,96.2-61.2,144.2c-20,48-39.5,95.7-58.5,143.2c-19.9,49.5-39.2,98.7-58,147.2
c-4.2,10.9-8.5,21.9-12.7,32.8c-15,39.2-29.7,77.8-44,116l12.7,25.1l11.4-1.2c0.4-1.1,0.8-2.3,1.3-3.4
C396.7,1905.4,414.9,1856.4,432.9,1808.8z"/>
<path fill="none" d="M980,1186.8L980,1186.8c0.1,0,0.1,0,0.1-0.1C980.1,1186.8,980.1,1186.8,980,1186.8z"/>
<path fill="#BE202E" d="M952.6,1323c-10.6,1.9-21.4,3.8-32.5,5.7c-0.1,0-0.1,0.1-0.2,0.1c5.6-0.8,11.2-1.7,16.6-2.6
C942,1325.2,947.3,1324.1,952.6,1323z"/>
<path opacity="0.35" fill="#BE202E" d="M952.6,1323c-10.6,1.9-21.4,3.8-32.5,5.7c-0.1,0-0.1,0.1-0.2,0.1c5.6-0.8,11.2-1.7,16.6-2.6
C942,1325.2,947.3,1324.1,952.6,1323z"/>
<path fill="#BE202E" d="M980.3,1186.7C980.2,1186.7,980.2,1186.7,980.3,1186.7c-0.1,0.1-0.2,0.1-0.2,0.1c1.8-0.2,3.5-0.5,5.2-0.8
c7-1,13.9-2.2,20.7-3.7C997.5,1183.8,989,1185.2,980.3,1186.7L980.3,1186.7L980.3,1186.7z"/>
<path opacity="0.35" fill="#BE202E" d="M980.3,1186.7C980.2,1186.7,980.2,1186.7,980.3,1186.7c-0.1,0.1-0.2,0.1-0.2,0.1
c1.8-0.2,3.5-0.5,5.2-0.8c7-1,13.9-2.2,20.7-3.7C997.5,1183.8,989,1185.2,980.3,1186.7L980.3,1186.7L980.3,1186.7z"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="-7537.7339" y1="-2391.4075" x2="-4625.4141" y2="-2391.4075" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 5117.8774 -2859.9343)">
<stop offset="0.3233" style="stop-color:#9E2064"/>
<stop offset="0.6302" style="stop-color:#C92037"/>
<stop offset="0.7514" style="stop-color:#CD2335"/>
<stop offset="1" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_2_)" d="M858.6,784.7c25.1-46.9,50.5-92.8,76.2-137.4c26.7-46.4,53.7-91.3,80.9-134.7
c1.6-2.6,3.2-5.2,4.8-7.7c27-42.7,54.2-83.7,81.6-122.9L1019.5,226c-6.2,7.6-12.5,15.3-18.8,23.2c-23.8,29.7-48.6,61.6-73.9,95.5
c-28.6,38.2-58,78.9-87.8,121.7c-27.6,39.5-55.5,80.9-83.5,123.7c-23.8,36.5-47.7,74-71.4,112.5c-0.9,1.4-1.8,2.9-2.6,4.3
l107.5,212.3C811.8,873.6,835.1,828.7,858.6,784.7z"/>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="-7186.1777" y1="-2099.3059" x2="-5450.7183" y2="-2099.3059" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 5117.8774 -2859.9343)">
<stop offset="0" style="stop-color:#282662"/>
<stop offset="9.548390e-02" style="stop-color:#662E8D"/>
<stop offset="0.7882" style="stop-color:#9F2064"/>
<stop offset="0.9487" style="stop-color:#CD2032"/>
</linearGradient>
<path fill="url(#SVGID_3_)" d="M369,1981c-14.2,39.1-28.5,78.9-42.9,119.6c-0.2,0.6-0.4,1.2-0.6,1.8c-2,5.7-4.1,11.5-6.1,17.2
c-9.7,27.4-18,52.1-37.3,108.2c31.7,14.5,57.1,52.5,81.1,95.6c-2.6-44.7-21-86.6-56.2-119.1c156.1,7,290.6-32.4,360.1-146.6
c6.2-10.2,11.9-20.9,17-32.2c-31.6,40.1-70.8,57.1-144.5,53c-0.2,0.1-0.3,0.1-0.5,0.2c0.2-0.1,0.3-0.1,0.5-0.2
c108.6-48.6,163.1-95.3,211.2-172.6c11.4-18.3,22.5-38.4,33.8-60.6c-94.9,97.5-205,125.3-320.9,104.2l-86.9,9.5
C374.4,1966.3,371.7,1973.6,369,1981z"/>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="-7374.1626" y1="-2418.5454" x2="-4461.8428" y2="-2418.5454" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 5117.8774 -2859.9343)">
<stop offset="0.3233" style="stop-color:#9E2064"/>
<stop offset="0.6302" style="stop-color:#C92037"/>
<stop offset="0.7514" style="stop-color:#CD2335"/>
<stop offset="1" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_4_)" d="M409.6,1786.3c18.8-48.5,38.1-97.7,58-147.2c19-47.4,38.5-95.2,58.5-143.2
c20-48,40.4-96.1,61.2-144.2c21.2-49,42.9-97.8,64.9-146.5c21.8-48.1,44-95.9,66.6-143.3c8.1-17.1,16.3-34.1,24.6-51
c14.2-29.3,28.6-58.4,43.1-87.2c0.8-1.6,1.6-3.1,2.4-4.6L681.4,706.8c-1.8,2.9-3.5,5.8-5.3,8.6c-25.1,40.9-50,82.7-74.4,125.4
c-24.7,43.1-49,87.1-72.7,131.7c-20,37.6-39.6,75.6-58.6,113.9c-3.8,7.8-7.6,15.5-11.3,23.2c-23.4,48.2-44.6,94.8-63.7,139.5
c-21.7,50.7-40.7,99.2-57.5,145.1c-11,30.2-21,59.4-30.1,87.4c-7.5,24-14.7,47.9-21.5,71.8c-16,56.3-29.9,112.4-41.2,168.3
L353,1935.1c14.3-38.1,28.9-76.8,44-116C401.1,1808.2,405.4,1797.3,409.6,1786.3z"/>
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="-7161.7642" y1="-2379.1431" x2="-5631.2524" y2="-2379.1431" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 5117.8774 -2859.9343)">
<stop offset="0" style="stop-color:#282662"/>
<stop offset="9.548390e-02" style="stop-color:#662E8D"/>
<stop offset="0.7882" style="stop-color:#9F2064"/>
<stop offset="0.9487" style="stop-color:#CD2032"/>
</linearGradient>
<path fill="url(#SVGID_5_)" d="M243.5,1729.4c-13.6,68.2-23.2,136.2-28,203.8c-0.2,2.4-0.4,4.7-0.5,7.1
c-33.7-54-124-106.8-123.8-106.2c64.6,93.7,113.7,186.7,120.9,278c-34.6,7.1-82-3.2-136.8-23.3c57.1,52.5,100,67,116.7,70.9
c-52.5,3.3-107.1,39.3-162.1,80.8c80.5-32.8,145.5-45.8,192.1-35.3C148.1,2414.2,74.1,2645,0,2890c22.7-6.7,36.2-21.9,43.9-42.6
c13.2-44.4,100.8-335.6,238-718.2c3.9-10.9,7.8-21.8,11.8-32.9c1.1-3,2.2-6.1,3.3-9.2c14.5-40.1,29.5-81.1,45.1-122.9
c3.5-9.5,7.1-19,10.7-28.6c0.1-0.2,0.1-0.4,0.2-0.6l-107.9-213.2C244.6,1724.4,244,1726.9,243.5,1729.4z"/>
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="-7374.1626" y1="-2117.1309" x2="-4461.8428" y2="-2117.1309" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 5117.8774 -2859.9343)">
<stop offset="0.3233" style="stop-color:#9E2064"/>
<stop offset="0.6302" style="stop-color:#C92037"/>
<stop offset="0.7514" style="stop-color:#CD2335"/>
<stop offset="1" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_6_)" d="M805.6,937c-3.1,6.3-6.2,12.7-9.3,19.1c-9.3,19.2-18.8,38.8-28.3,58.8
c-10.3,21.7-20.7,43.9-31.3,66.5c-5.3,11.4-10.6,22.9-16,34.6c-16.1,35-32.4,71.1-48.9,108.3c-20.3,45.8-41,93.4-62,142.8
c-20,47.1-40.3,95.9-60.9,146.3c-19.5,47.9-39.3,97.3-59.3,148.3c-17.8,45.4-35.9,92.1-54,140.1c-0.9,2.3-1.8,4.7-2.6,7
c-18,47.6-36.2,96.6-54.6,146.8c-0.4,1.1-0.8,2.3-1.3,3.4l86.9-9.5c-1.7-0.3-3.5-0.5-5.2-0.9c103.9-13,242.1-90.6,331.4-186.5
c41.1-44.2,78.5-96.3,113-157.3c25.7-45.4,49.8-95.8,72.8-151.5c20.1-48.7,39.4-101.4,58-158.6c-23.9,12.6-51.2,21.8-81.4,28.2
c-5.3,1.1-10.7,2.2-16.1,3.1c-5.5,1-11,1.8-16.6,2.6l0,0l0,0c0.1,0,0.1-0.1,0.2-0.1c96.9-37.3,158-109.2,202.4-197.4
c-25.5,17.4-66.9,40.1-116.6,51.1c-6.7,1.5-13.7,2.7-20.7,3.7c-1.7,0.3-3.5,0.6-5.2,0.8l0,0l0,0c0.1,0,0.1,0,0.1-0.1
c0,0,0.1,0,0.1,0l0,0c33.6-14.1,62-29.8,86.6-48.4c5.3-4,10.4-8.1,15.3-12.3c7.5-6.5,14.7-13.3,21.5-20.5c4.4-4.6,8.6-9.3,12.7-14.2
c9.6-11.5,18.7-23.9,27.1-37.3c2.6-4.1,5.1-8.3,7.6-12.6c3.2-6.2,6.3-12.3,9.3-18.3c13.5-27.2,24.4-51.5,33-72.8
c4.3-10.6,8.1-20.5,11.3-29.7c1.3-3.7,2.5-7.2,3.7-10.6c3.4-10.2,6.2-19.3,8.4-27.3c3.3-12,5.3-21.5,6.4-28.4l0,0l0,0
c-3.3,2.6-7.1,5.2-11.3,7.7c-29.3,17.5-79.5,33.4-119.9,40.8l79.8-8.8l-79.8,8.8c-0.6,0.1-1.2,0.2-1.8,0.3c-4,0.7-8.1,1.3-12.2,2
c0.7-0.3,1.4-0.7,2.1-1l-273,29.9C806.6,935,806.1,936,805.6,937z"/>
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="-7554.8232" y1="-2132.0981" x2="-4642.5034" y2="-2132.0981" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 5117.8774 -2859.9343)">
<stop offset="0.3233" style="stop-color:#9E2064"/>
<stop offset="0.6302" style="stop-color:#C92037"/>
<stop offset="0.7514" style="stop-color:#CD2335"/>
<stop offset="1" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_7_)" d="M1112.9,385.1c-24.3,37.3-50.8,79.6-79.4,127.5c-1.5,2.5-3,5.1-4.5,7.6
c-24.6,41.5-50.8,87.1-78.3,137c-23.8,43.1-48.5,89.3-74.3,139c-22.4,43.3-45.6,89.2-69.4,137.8l273-29.9
c79.5-36.6,115.1-69.7,149.6-117.6c9.2-13.2,18.4-27,27.5-41.3c28-43.8,55.6-92,80.1-139.9c23.7-46.3,44.7-92.2,60.7-133.5
c10.2-26.3,18.4-50.8,24.1-72.3c5-19,8.9-36.9,11.9-54.1C1327.9,363.5,1197.6,380.9,1112.9,385.1z"/>
<path fill="#BE202E" d="M936.5,1326.1c-5.5,1-11,1.8-16.6,2.6l0,0C925.5,1328,931,1327.1,936.5,1326.1z"/>
<path opacity="0.35" fill="#BE202E" d="M936.5,1326.1c-5.5,1-11,1.8-16.6,2.6l0,0C925.5,1328,931,1327.1,936.5,1326.1z"/>
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="-7374.1626" y1="-2027.484" x2="-4461.8433" y2="-2027.484" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 5117.8774 -2859.9343)">
<stop offset="0.3233" style="stop-color:#9E2064"/>
<stop offset="0.6302" style="stop-color:#C92037"/>
<stop offset="0.7514" style="stop-color:#CD2335"/>
<stop offset="1" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_8_)" d="M936.5,1326.1c-5.5,1-11,1.8-16.6,2.6l0,0C925.5,1328,931,1327.1,936.5,1326.1z"/>
<path fill="#BE202E" d="M980,1186.8c1.8-0.2,3.5-0.5,5.2-0.8C983.5,1186.3,981.8,1186.6,980,1186.8L980,1186.8z"/>
<path opacity="0.35" fill="#BE202E" d="M980,1186.8c1.8-0.2,3.5-0.5,5.2-0.8C983.5,1186.3,981.8,1186.6,980,1186.8L980,1186.8z"/>
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="-7374.1626" y1="-2037.7417" x2="-4461.8433" y2="-2037.7417" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 5117.8774 -2859.9343)">
<stop offset="0.3233" style="stop-color:#9E2064"/>
<stop offset="0.6302" style="stop-color:#C92037"/>
<stop offset="0.7514" style="stop-color:#CD2335"/>
<stop offset="1" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_9_)" d="M980,1186.8c1.8-0.2,3.5-0.5,5.2-0.8C983.5,1186.3,981.8,1186.6,980,1186.8L980,1186.8z"/>
<path fill="#BE202E" d="M980.2,1186.7C980.2,1186.7,980.2,1186.7,980.2,1186.7L980.2,1186.7L980.2,1186.7L980.2,1186.7
C980.2,1186.7,980.2,1186.7,980.2,1186.7z"/>
<path opacity="0.35" fill="#BE202E" d="M980.2,1186.7C980.2,1186.7,980.2,1186.7,980.2,1186.7L980.2,1186.7L980.2,1186.7
L980.2,1186.7C980.2,1186.7,980.2,1186.7,980.2,1186.7z"/>
<linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="-5738.0635" y1="-2039.799" x2="-5094.3457" y2="-2039.799" gradientTransform="matrix(0.4226 -0.9063 0.9063 0.4226 5117.8774 -2859.9343)">
<stop offset="0.3233" style="stop-color:#9E2064"/>
<stop offset="0.6302" style="stop-color:#C92037"/>
<stop offset="0.7514" style="stop-color:#CD2335"/>
<stop offset="1" style="stop-color:#E97826"/>
</linearGradient>
<path fill="url(#SVGID_10_)" d="M980.2,1186.7C980.2,1186.7,980.2,1186.7,980.2,1186.7L980.2,1186.7L980.2,1186.7L980.2,1186.7
C980.2,1186.7,980.2,1186.7,980.2,1186.7z"/>
</svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 525">
<defs>
<style>
.cls-1 {
fill: #7c297d;
}
.cls-2 {
fill: #f79a23;
}
.cls-3 {
fill: #dd552c;
}
.cls-4 {
fill: #d22128;
}
</style>
</defs>
<g>
<g>
<path class="cls-1" d="M458.8041825,181.7454822h49.0451573l87.4128103,220.0939304h-53.4548455l-14.7775045-38.0445002h-87.4060779l-15.0939249,38.0445002h-53.1384251l87.4128103-220.0939304ZM514.4537735,324.4914631l-31.1236462-80.4919562-30.8139581,80.4919562h61.9376043Z"/>
<path class="cls-1" d="M622.7166664,331.7220052h48.7354693c.3164204,21.0655178,16.0364536,33.3318564,40.2459781,33.3318564,20.7490974,0,33.9579648-8.4894912,33.9579648-23.2669957,0-15.4103452-14.1513961-24.5259448-45.2750423-31.4467989-51.5697879-10.6909691-76.0957328-32.0661749-76.0957328-68.2256176,0-39.9295577,31.7564869-64.7719229,82.3837461-64.7719229,52.189164,0,85.5210204,28.2960599,85.5210204,72.6285734h-48.1093609c0-19.8065687-13.8349757-31.7564869-36.7855511-31.7564869-21.0655178,0-33.9579648,8.8059116-33.9579648,23.2669957,0,13.5252877,11.6334979,22.0147789,45.2750423,29.8714294,54.7137946,12.892447,77.6643699,33.6415444,77.6643699,68.542038,0,41.5049272-32.0729073,66.3472924-84.2620713,66.3472924-55.339903,0-89.2978678-28.9289006-89.2978678-74.5203633Z"/>
<path class="cls-1" d="M852.9091193,181.7454822h137.7169167v45.2750423h-87.7224983v55.0234826h79.8591155v44.0228255h-79.8591155v75.7725801h-49.9944184v-220.0939304Z"/>
</g>
<g>
<path class="cls-3" d="M141.1077177,209.8881008c13.1869872-32.608129,27.9005344-65.1523006,43.6878912-93.9970469-23.5665852-17.5041056-46.1956909-55.2490163-55.4930639-71.9721694-3.3392448,3.7936783-5.5070609,8.1629723-6.4899838,11.7176522-8.7587851,31.6252061,22.4204029,69.8144518-2.6929393,55.86166-20.9241385-11.6267655-68.040478-37.0885068-86.0124818-11.7816095,20.1280383,25.862316,72.7733189,90.9304622,107.0005776,110.1715137Z"/>
<path class="cls-2" d="M184.7956089,115.8910539c15.2975784-27.9493439,31.5999598-52.4281623,48.5015202-70.0635487,0,0-16.8578001,24.4013964-40.9360438,73.180626,14.5822664,4.0158458,56.1124399,12.2562401,113.9601427-2.686207,1.4222086-10.5125619-5.645074-22.0753701-40.8636711-25.9464703-22.9926525-2.5246306,27.5975787-54.9023003-9.0516423-79.6739759-1.1832102-.8011494-2.3462234-1.508046-3.4856733-2.1408868-1.2286536-.4409688-2.5263137-.8449097-3.9114944-1.2050903-42.8025874-11.1689658-48.7354693,61.409115-65.8154369,45.8068978-27.6009449-25.2126444-45.0225792-19.3218396-53.890765-9.2435142,9.297373,16.7231532,31.9264787,54.4680639,55.4930639,71.9721694Z"/>
<path class="cls-4" d="M106.6818547,303.8380213c9.6626029-28.9457315,21.2860022-61.4663399,34.425863-93.9499204-34.2272587-19.2410514-86.8725393-84.3091977-107.0005776-110.1715137-3.5933909,5.0559936-6.0490149,12.1081284-6.8669953,21.7522173-4.3894911,51.802054,49.0518897,90.152876,38.3979485,97.1578844-14.0924881,9.2670774-42.1411341-22.2605096-53.19565-2.2284073,16.0229889,20.5841549,48.6614135,57.7770131,94.2394114,87.4397396Z"/>
<path class="cls-3" d="M254.7564893,192.1180675c-27.0589909-9.6508213,28.2758628-35.5333343,46.2445004-62.9912168,2.2990969-3.5109196,4.6756159-8.0485224,5.3202382-12.8049265-57.8477027,14.942447-99.3778763,6.7020527-113.9601427,2.686207-12.4649429,25.2564046-26.8587035,57.0835812-41.8701571,95.4579665,15.6341958,6.6852219,79.4703224,31.3794754,169.6366631,31.50739,15.1595653-39.4852227-39.6905594-44.6994265-65.3711019-53.8554202Z"/>
<path class="cls-4" d="M117.1371916,309.0488589c15.9001236,4.9550083,68.5841152,19.7560761,124.5366618,17.5276688,7.5166669-20.3518889-20.585838-22.3379316-22.8748364-38.6605101-1.7722907-12.6298854,73.9565291,10.6135471,98.3225807-35.3111668,1.2320197-2.3226602,2.1930625-4.5140395,3.0059935-6.6313631-90.1663407-.1279146-154.0024672-24.8221682-169.6366631-31.50739-10.9703615,28.0435968-22.2554604,59.5173251-33.3537365,94.5827612Z"/>
<path class="cls-1" d="M117.1371916,309.0488589c-7.1447046,22.5701977-14.1934733,46.6821031-21.0352222,72.1640414-2.4270116,9.0348114-4.8254106,18.2379315-7.190148,27.6430221,53.1064464,17.531035,102.0085414.0403941,103.6663821-21.6377674.0134647-.1716749-.018514-.2995895-.0084154-.464532,1.2639984-22.9606739-33.150083-10.2331694-32.3354689-24.0378496.8179803-13.9056654,60.0811593-.0807882,78.4082944-29.8949926,1.4053777-2.2856322,2.3209771-4.3154353,3.0312398-6.244253-55.9525467,2.2284073-108.6365383-12.5726604-124.5366618-17.5276688Z"/>
<path class="cls-1" d="M12.4424433,216.3982816c-.7742201,1.4036946-1.4727012,3.0261906-2.068514,4.9651069-10.2920775,33.4496725,62.4846076,78.3847312,52.5830064,88.180298-8.9254108,8.8261086-20.5572255-11.3440069-34.9004936-3.0127259-1.5720033.9155994-3.1675699,2.0701971-4.8052136,3.749918-16.2215932,16.6188018-.2541461,64.4958967,45.7698699,90.0518908-10.7397786,36.0618236-21.4324308,76.351562-31.8995493,118.4859638,3.7970444-1.3296388,8.3413795-2.6626438,9.4724141-6.244253,1.6982349-6.9814452,12.1939658-49.4625629,29.2352225-107.0948305,0,0,3.1103449-11.8051727,8.8042285-31.254927,5.2646964-17.9854685,12.7460184-42.5147795,22.0484407-70.3867014-45.577998-29.6627266-78.2164225-66.8555847-94.2394114-87.4397396Z"/>
</g>
</g>
<g>
<polygon class="cls-1" points="404.1671788 144.7351535 413.43089 144.7351535 413.43089 82.5349876 436.7786739 82.5349876 436.7786739 73.4732468 380.8193949 73.4732468 380.8193949 82.5349876 404.1671788 82.5349876 404.1671788 144.7351535"/>
<polygon class="cls-1" points="464.8525663 112.9584696 501.321697 112.9584696 501.321697 144.7351535 510.6796611 144.7351535 510.6796611 73.4732468 501.321697 73.4732468 501.321697 103.8899964 464.8525663 103.8899964 464.8525663 73.4732468 455.5955874 73.4732468 455.5955874 144.7351535 464.8525663 144.7351535 464.8525663 112.9584696"/>
<polygon class="cls-1" points="581.40298 135.9629037 546.2331925 135.9629037 546.2331925 112.8574844 577.8213707 112.8574844 577.8213707 104.0852345 546.2331925 104.0852345 546.2331925 82.1512438 580.3392689 82.1512438 580.3392689 73.4732468 536.9762136 73.4732468 536.9762136 144.7351535 581.40298 144.7351535 581.40298 135.9629037"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

@@ -20,6 +20,32 @@ spring:
name: ${HOSTNAME:@hertzbeat@}${PID}
profiles:
active: prod
ai:
mcp:
server:
enabled: true
stdio: false
name: sse-mcp-server
version: 1.0.0
resource-change-notification: true
tool-change-notification: true
prompt-change-notification: true
sse-endpoint: /api/sse
sse-message-endpoint: /api/mcp/message
type: SYNC
capabilities:
tool: true
resource: true
prompt: true
completion: true
chat:
client:
enabled: false
openai:
api-key: OPENAI_API_KEY
chat:
options:
model: gpt-4.1-nano-2025-04-14
mvc:
static-path-pattern: /**
jackson:
@@ -222,3 +248,10 @@ grafana:
url: http://127.0.0.1:3000
username: admin
password: admin
ai:
type:
model:
api-key:
api-secret:
api-url:
@@ -20,6 +20,32 @@ spring:
name: ${HOSTNAME:@hertzbeat@}${PID}
profiles:
active: prod
ai:
mcp:
server:
enabled: true
stdio: false
name: sse-mcp-server
version: 1.0.0
resource-change-notification: true
tool-change-notification: true
prompt-change-notification: true
sse-endpoint: /api/sse
sse-message-endpoint: /api/mcp/message
type: SYNC
capabilities:
tool: true
resource: true
prompt: true
completion: true
chat:
client:
enabled: false
openai:
api-key: OPENAI_API_KEY
chat:
options:
model: gpt-4.1-nano-2025-04-14
mvc:
static-path-pattern: /**
jackson:
@@ -220,3 +246,10 @@ grafana:
username: admin
password: admin
ai:
type:
model:
api-key:
api-secret:
api-url:
@@ -20,6 +20,32 @@ spring:
name: ${HOSTNAME:@hertzbeat@}${PID}
profiles:
active: prod
ai:
mcp:
server:
enabled: true
stdio: false
name: sse-mcp-server
version: 1.0.0
resource-change-notification: true
tool-change-notification: true
prompt-change-notification: true
sse-endpoint: /api/sse
sse-message-endpoint: /api/mcp/message
type: SYNC
capabilities:
tool: true
resource: true
prompt: true
completion: true
chat:
client:
enabled: false
openai:
api-key: OPENAI_API_KEY
chat:
options:
model: gpt-4.1-nano-2025-04-14
mvc:
static-path-pattern: /**
jackson:
@@ -222,3 +248,10 @@ grafana:
url: http://127.0.0.1:3000
username: admin
password: admin
ai:
type:
model:
api-key:
api-secret:
api-url:
@@ -20,6 +20,32 @@ spring:
name: ${HOSTNAME:@hertzbeat@}${PID}
profiles:
active: prod
ai:
mcp:
server:
enabled: true
stdio: false
name: sse-mcp-server
version: 1.0.0
resource-change-notification: true
tool-change-notification: true
prompt-change-notification: true
sse-endpoint: /api/sse
sse-message-endpoint: /api/mcp/message
type: SYNC
capabilities:
tool: true
resource: true
prompt: true
completion: true
chat:
client:
enabled: false
openai:
api-key: OPENAI_API_KEY
chat:
options:
model: gpt-4.1-nano-2025-04-14
mvc:
static-path-pattern: /**
jackson:
@@ -221,3 +247,10 @@ grafana:
url: http://127.0.0.1:3000
username: admin
password: admin
ai:
type:
model:
api-key:
api-secret:
api-url:
+2 -2
View File
@@ -17,7 +17,7 @@
FROM openjdk:21-slim-bullseye
MAINTAINER Apache HertzBeat "dev@hertzbeat.apache.org"
LABEL maintainer="dev@hertzbeat.apache.org"
ARG VERSION
@@ -27,7 +27,7 @@ RUN mkdir /var/run/sshd
ADD apache-hertzbeat-collector-*-bin.tar.gz /opt/
ENV JAVA_OPTS ""
ENV JAVA_OPTS=""
ENV TZ=Asia/Shanghai
ENV LANG=en_US.UTF-8
+1 -1
View File
@@ -17,7 +17,7 @@
FROM openjdk:21-slim-bullseye
MAINTAINER Apache HertzBeat "dev@hertzbeat.apache.org"
LABEL maintainer="dev@hertzbeat.apache.org"
# Install SSH And Locales
RUN apt-get update && apt-get install -y openssh-server && apt-get install -y locales
+35 -144
View File
@@ -133,6 +133,7 @@
z-index: 11 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
/* Fix notification badge positioning */
@@ -328,174 +329,64 @@ global-footer {
min-height: 120px;
}
/* AI Assistant Chatbot styles */
.ai-chatbot-container {
/* AI Chat Button styles */
.ai-chat-button-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
}
.ai-chatbot-button {
width: 50px;
height: 50px;
.ai-chat-button {
width: 56px;
height: 56px;
border-radius: 50%;
background-color: @primary-color; /* Use system theme color */
background-color: @primary-color;
color: white;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: all 0.3s;
transition: all 0.3s ease;
}
.ai-chatbot-button:hover {
.ai-chat-button:hover {
transform: scale(1.05);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}
/* Robot icon styles */
.robot-icon {
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
.ai-chat-button:active {
transform: scale(0.95);
}
.robot-icon svg {
width: 30px;
height: 30px;
fill: white; /* Ensure SVG is white */
}
/* AI Chat Modal Styles */
:host ::ng-deep .ai-chat-modal {
.ant-modal {
border-radius: 16px !important;
overflow: hidden !important;
}
.ant-modal-content {
border-radius: 16px !important;
overflow: hidden !important;
}
.ai-chatbot-window {
position: absolute;
bottom: 70px;
right: 0;
width: 350px;
height: 500px;
border-radius: 8px;
background-color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
overflow: hidden;
transition: all 0.3s;
}
.ai-chatbot-window.maximized {
width: 80vw;
height: 80vh;
bottom: 10vh;
right: 10vw;
}
.ant-modal-header {
border-radius: 16px 16px 0 0 !important;
margin: 0 !important;
padding: 16px !important;
}
.ant-modal-body {
border-radius: 0 0 16px 16px !important;
padding: 0 !important;
}
.chatbot-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: @primary-color; /* Use system theme color */
color: white;
}
.chatbot-title {
font-weight: 500;
display: flex;
align-items: center;
}
.chatbot-controls {
display: flex;
gap: 12px;
}
.control-item {
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.3s;
}
.control-item:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.chatbot-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
background-color: #f5f5f5;
}
.message {
max-width: 80%;
padding: 8px 12px;
border-radius: 12px;
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.user-message {
align-self: flex-end;
background-color: @primary-color; /* Use system theme color */
color: white;
}
.bot-message {
align-self: flex-start;
background-color: white;
color: #333;
border: 1px solid #e8e8e8;
}
.streaming-message {
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.2); }
70% { box-shadow: 0 0 0 6px rgba(0, 0, 0, 0); }
100% { box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); }
}
.message-content {
word-break: break-word;
white-space: pre-wrap;
}
.message-time {
font-size: 12px;
margin-top: 4px;
opacity: 0.7;
text-align: right;
}
.loading-message {
padding: 12px;
text-align: center;
}
.chatbot-input {
display: flex;
padding: 12px;
border-top: 1px solid #e8e8e8;
background-color: white;
}
.chatbot-input input {
flex: 1;
margin-right: 8px;
.ant-modal-close {
top: 8px !important;
right: 8px !important;
}
}
+10 -175
View File
@@ -3,11 +3,10 @@ import { I18NService } from '@core';
import { ALAIN_I18N_TOKEN, SettingsService, User } from '@delon/theme';
import { LayoutDefaultOptions } from '@delon/theme/layout-default';
import { environment } from '@env/environment';
import { Observable, Subject, of } from 'rxjs';
import { delay, tap, finalize, catchError, takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { CONSTANTS } from '../../shared/constants';
import { AiBotService, ChatMessage } from '../../shared/services/ai-bot.service';
import { AiChatModalService } from '../../shared/services/ai-chat-modal.service';
@Component({
selector: 'layout-basic',
@@ -75,9 +74,9 @@ import { AiBotService, ChatMessage } from '../../shared/services/ai-bot.service'
</global-footer>
<setting-drawer *ngIf="showSettingDrawer"></setting-drawer>
<!-- AI Chatbot -->
<div class="ai-chatbot-container">
<div class="ai-chatbot-button" (click)="toggleChatbot()" *ngIf="!isChatbotOpen">
<!-- AI Chat Button -->
<div class="ai-chat-button-container">
<div class="ai-chat-button" (click)="openChatModal()">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
@@ -91,69 +90,11 @@ import { AiBotService, ChatMessage } from '../../shared/services/ai-bot.service'
/>
</svg>
</div>
<div class="ai-chatbot-window" *ngIf="isChatbotOpen" [class.maximized]="isChatbotMaximized">
<div class="chatbot-header">
<div class="chatbot-title">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="20"
height="20"
fill="white"
style="margin-right: 6px; vertical-align: middle;"
>
<path
d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2M7.5 13A2.5 2.5 0 0 0 5 15.5A2.5 2.5 0 0 0 7.5 18a2.5 2.5 0 0 0 2.5-2.5A2.5 2.5 0 0 0 7.5 13m9 0a2.5 2.5 0 0 0-2.5 2.5a2.5 2.5 0 0 0 2.5 2.5a2.5 2.5 0 0 0 2.5-2.5a2.5 2.5 0 0 0-2.5-2.5z"
/>
</svg>
{{ 'ai.bot.title' | i18n }}
</div>
<div class="chatbot-controls">
<span class="control-item" (click)="toggleMaximize()" title="{{ isChatbotMaximized ? '还原' : '最大化' }}">
<i nz-icon [nzType]="isChatbotMaximized ? 'fullscreen-exit' : 'fullscreen'" nzTheme="outline"></i>
</span>
<span class="control-item" (click)="toggleChatbot()" title="关闭">
<i nz-icon nzType="close" nzTheme="outline"></i>
</span>
</div>
</div>
<div class="chatbot-messages" #chatMessagesContainer>
<div
*ngFor="let message of chatMessages"
[class.user-message]="message.isUser"
[class.bot-message]="!message.isUser"
class="message"
>
<div class="message-content">{{ message.content }}</div>
<div class="message-time">{{ message.timestamp | date : 'HH:mm' }}</div>
</div>
<div *ngIf="currentBotMessage && isLoading" class="bot-message streaming-message">
<div class="message-content">{{ currentBotMessage.content }}</div>
<div class="message-time">{{ currentBotMessage.timestamp | date : 'HH:mm' }}</div>
</div>
<div *ngIf="isLoading && !currentBotMessage" class="bot-message loading-message">
<nz-spin nzSimple></nz-spin>
</div>
</div>
<div class="chatbot-input">
<input
nz-input
placeholder="{{ 'ai.bot.input.placeholder' | i18n }}"
[(ngModel)]="currentMessage"
(keyup.enter)="sendMessage()"
[disabled]="isLoading"
/>
<button nz-button nzType="primary" [disabled]="!currentMessage.trim() || isLoading" (click)="sendMessage()">
{{ 'ai.bot.send' | i18n }}
</button>
</div>
</div>
</div>
`,
styleUrls: ['./basic.component.less']
})
export class LayoutBasicComponent implements OnInit, OnDestroy {
export class LayoutBasicComponent implements OnDestroy {
options: LayoutDefaultOptions = {
logoExpanded: `./assets/brand_white.svg`,
logoCollapsed: `./assets/logo.svg`
@@ -163,6 +104,7 @@ export class LayoutBasicComponent implements OnInit, OnDestroy {
showSettingDrawer = !environment.production;
version = CONSTANTS.VERSION;
currentYear = new Date().getFullYear();
get user(): User {
return this.settings.user;
}
@@ -177,127 +119,20 @@ export class LayoutBasicComponent implements OnInit, OnDestroy {
}
}
// AI Chatbot related properties
isChatbotOpen = false;
isChatbotMaximized = false;
chatMessages: ChatMessage[] = [];
currentMessage = '';
isLoading = false;
currentBotMessage: ChatMessage | null = null;
// For subscription cleanup
private destroy$ = new Subject<void>();
constructor(
private settings: SettingsService,
@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService,
private aiBotService: AiBotService
private aiChatModalService: AiChatModalService
) {}
ngOnInit(): void {
// Initialize welcome message
this.chatMessages.push({
content: this.i18nSvc.fanyi('ai.bot.greeting'),
isUser: false,
timestamp: new Date()
});
console.log('AI Chatbot initialization completed');
}
ngOnDestroy(): void {
// Cancel all subscriptions when component is destroyed
this.destroy$.next();
this.destroy$.complete();
}
toggleChatbot(): void {
this.isChatbotOpen = !this.isChatbotOpen;
if (!this.isChatbotOpen) {
setTimeout(() => {
this.isChatbotMaximized = false;
}, 300);
} else {
// Scroll to bottom when window opens
setTimeout(() => this.scrollToBottom(), 100);
}
console.log('Toggle chatbot status:', this.isChatbotOpen ? 'open' : 'closed');
}
toggleMaximize(): void {
setTimeout(() => {
this.isChatbotMaximized = !this.isChatbotMaximized;
console.log('Chat window maximize status:', this.isChatbotMaximized ? 'maximized' : 'normal');
}, 10);
}
sendMessage(): void {
if (!this.currentMessage.trim() || this.isLoading) return;
// Add user message
this.chatMessages.push({
content: this.currentMessage,
isUser: true,
timestamp: new Date()
});
const userMessage = this.currentMessage;
this.currentMessage = '';
this.isLoading = true;
this.currentBotMessage = null;
// Ensure scrolling to bottom after message display
setTimeout(() => this.scrollToBottom(), 100);
// Call AI service to get response
this.aiBotService
.sendMessage(userMessage)
.pipe(
takeUntil(this.destroy$),
finalize(() => {
this.isLoading = false;
// If there is a current message, add it to chat history
if (this.currentBotMessage) {
this.chatMessages.push({ ...this.currentBotMessage });
this.currentBotMessage = null;
}
// Ensure scrolling to bottom after message display
setTimeout(() => this.scrollToBottom(), 100);
})
)
.subscribe({
next: response => {
console.log('Received AI response update:', response);
// Update currently receiving message
this.currentBotMessage = response;
// Scroll to bottom in real-time
this.scrollToBottom();
},
error: error => {
console.error('AI response error:', error);
// Add error message
this.chatMessages.push({
content: this.i18nSvc.fanyi('ai.bot.connect-fail'),
isUser: false,
timestamp: new Date()
});
}
});
}
// Scroll to bottom of messages
private scrollToBottom(): void {
try {
const chatMessages = document.querySelector('.chatbot-messages');
if (chatMessages) {
chatMessages.scrollTop = chatMessages.scrollHeight;
}
} catch (err) {
console.error('Failed to scroll to bottom:', err);
}
openChatModal(): void {
this.aiChatModalService.openChatModal();
}
}
@@ -1,6 +1,6 @@
.search-input-group {
margin: 10px 0;
border-radius: 8px;
border-radius: 16px !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
@@ -18,428 +18,504 @@
-->
<div class="mobile-hide">
<ngx-slick-carousel nz-row class="carousel" [config]="slideConfig">
<div style="width: 25%; margin: 3px" ngxSlickItem class="slide">
<div class="mb-md hoverCard">
<div nz-row nzAlign="middle" class="bg-primary rounded-lg">
<div nz-col nzSpan="9" class="p-md text-white">
<div class="h2 mt0 font-weight-bold">{{ appCountService.size }}</div>
<p class="h5 text-nowrap mb0">
<i nz-icon nzType="cloud" nzTheme="outline"></i>
{{ 'monitor.category.service' | i18n }}
</p>
</div>
<div nz-col nzSpan="15">
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.up' | i18n }} </span><span style="font-weight: bolder">{{ appCountService.availableSize }}</span>
</nz-tag>
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.down' | i18n }} </span><span style="font-weight: bolder">{{ appCountService.unAvailableSize }}</span>
</nz-tag>
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.paused' | i18n }} </span><span style="font-weight: bolder">{{ appCountService.unManageSize }}</span>
</nz-tag>
<div nz-row nzGutter="16">
<div nz-col [nzXs]="{ span: 24 }" [nzSm]="{ span: 24 }" [nzMd]="{ span: 24 }" style="height: 100%">
<ngx-slick-carousel nz-row class="carousel" [config]="slideConfig">
<div style="width: 25%; margin: 3px" ngxSlickItem class="slide">
<div class="hoverCard">
<div nz-row nzAlign="middle" class="bg-primary dashboard-rounded">
<div nz-col nzSpan="9" class="p-md text-white">
<div class="h2 mt0 font-weight-bold">{{ appCountService.size }}</div>
<p class="h5 text-nowrap mb0">
<i nz-icon nzType="cloud" nzTheme="outline"></i>
{{ 'monitor.category.service' | i18n }}
</p>
</div>
<div nz-col nzSpan="15">
<div class="dashboard-slider-tag">
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.up' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountService.availableSize }}</span>
</nz-tag>
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.down' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountService.unAvailableSize }}</span>
</nz-tag>
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.paused' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountService.unManageSize }}</span>
</nz-tag>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div style="width: 25%; margin: 3px" ngxSlickItem class="slide">
<div class="mb-md hoverCard">
<div nz-row nzAlign="middle" class="bg-primary rounded-lg">
<div nz-col nzSpan="9" class="p-md text-white">
<div class="h2 mt0 font-weight-bold">{{ appCountProgram.size }}</div>
<p class="h5 text-nowrap mb0">
<i nz-icon nzType="code" nzTheme="outline"></i>
{{ 'monitor.category.program' | i18n }}
</p>
</div>
<div nz-col nzSpan="15">
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.up' | i18n }} </span><span style="font-weight: bolder">{{ appCountProgram.availableSize }}</span>
</nz-tag>
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.down' | i18n }} </span><span style="font-weight: bolder">{{ appCountProgram.unAvailableSize }}</span>
</nz-tag>
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.paused' | i18n }} </span><span style="font-weight: bolder">{{ appCountProgram.unManageSize }}</span>
</nz-tag>
<div style="width: 25%; margin: 3px" ngxSlickItem class="slide">
<div class="hoverCard">
<div nz-row nzAlign="middle" class="bg-primary dashboard-rounded">
<div nz-col nzSpan="9" class="p-md text-white">
<div class="h2 mt0 font-weight-bold">{{ appCountProgram.size }}</div>
<p class="h5 text-nowrap mb0">
<i nz-icon nzType="code" nzTheme="outline"></i>
{{ 'monitor.category.program' | i18n }}
</p>
</div>
<div nz-col nzSpan="15">
<div class="dashboard-slider-tag">
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.up' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountProgram.availableSize }}</span>
</nz-tag>
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.down' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountProgram.unAvailableSize }}</span>
</nz-tag>
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.paused' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountProgram.unManageSize }}</span>
</nz-tag>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div style="width: 25%; margin: 3px" ngxSlickItem class="slide">
<div class="mb-md hoverCard">
<div nz-row nzAlign="middle" class="bg-primary rounded-lg">
<div nz-col nzSpan="9" class="p-md text-white">
<div class="h2 mt0 font-weight-bold">{{ appCountDb.size }}</div>
<p class="h5 text-nowrap mb0">
<i nz-icon nzType="database" nzTheme="outline"></i>
{{ 'monitor.category.db' | i18n }}
</p>
</div>
<div nz-col nzSpan="15">
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.up' | i18n }} </span><span style="font-weight: bolder">{{ appCountDb.availableSize }}</span>
</nz-tag>
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.down' | i18n }} </span><span style="font-weight: bolder">{{ appCountDb.unAvailableSize }}</span>
</nz-tag>
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.paused' | i18n }} </span><span style="font-weight: bolder">{{ appCountDb.unManageSize }}</span>
</nz-tag>
<div style="width: 25%; margin: 3px" ngxSlickItem class="slide">
<div class="hoverCard">
<div nz-row nzAlign="middle" class="bg-primary dashboard-rounded">
<div nz-col nzSpan="9" class="p-md text-white">
<div class="h2 mt0 font-weight-bold">{{ appCountDb.size }}</div>
<p class="h5 text-nowrap mb0">
<i nz-icon nzType="database" nzTheme="outline"></i>
{{ 'monitor.category.db' | i18n }}
</p>
</div>
<div nz-col nzSpan="15">
<div class="dashboard-slider-tag">
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.up' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountDb.availableSize }}</span>
</nz-tag>
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.down' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountDb.unAvailableSize }}</span>
</nz-tag>
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.paused' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountDb.unManageSize }}</span>
</nz-tag>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div style="width: 25%; margin: 3px" ngxSlickItem class="slide">
<div class="mb-md hoverCard">
<div nz-row nzAlign="middle" class="bg-primary rounded-lg">
<div nz-col nzSpan="9" class="p-md text-white">
<div class="h2 mt0 font-weight-bold">{{ appCountCache.size }}</div>
<p class="h5 text-nowrap mb0">
<i nz-icon nzType="group" nzTheme="outline"></i>
{{ 'monitor.category.cache' | i18n }}
</p>
</div>
<div nz-col nzSpan="15">
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.up' | i18n }} </span><span style="font-weight: bolder">{{ appCountCache.availableSize }}</span>
</nz-tag>
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.down' | i18n }} </span><span style="font-weight: bolder">{{ appCountCache.unAvailableSize }}</span>
</nz-tag>
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.paused' | i18n }} </span><span style="font-weight: bolder">{{ appCountCache.unManageSize }}</span>
</nz-tag>
<div style="width: 25%; margin: 3px" ngxSlickItem class="slide">
<div class="hoverCard">
<div nz-row nzAlign="middle" class="bg-primary dashboard-rounded">
<div nz-col nzSpan="9" class="p-md text-white">
<div class="h2 mt0 font-weight-bold">{{ appCountCache.size }}</div>
<p class="h5 text-nowrap mb0">
<i nz-icon nzType="group" nzTheme="outline"></i>
{{ 'monitor.category.cache' | i18n }}
</p>
</div>
<div nz-col nzSpan="15">
<div class="dashboard-slider-tag">
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.up' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountCache.availableSize }}</span>
</nz-tag>
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.down' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountCache.unAvailableSize }}</span>
</nz-tag>
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.paused' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountCache.unManageSize }}</span>
</nz-tag>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div style="width: 25%; margin: 3px" ngxSlickItem class="slide">
<div class="mb-md hoverCard">
<div nz-row nzAlign="middle" class="bg-primary rounded-lg">
<div nz-col nzSpan="9" class="p-md text-white">
<div class="h2 mt0 font-weight-bold">{{ appCountOs.size }}</div>
<p class="h5 text-nowrap mb0">
<i nz-icon nzType="windows" nzTheme="outline"></i>
{{ 'monitor.category.os' | i18n }}
</p>
</div>
<div nz-col nzSpan="15">
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.up' | i18n }} </span><span style="font-weight: bolder">{{ appCountOs.availableSize }}</span>
</nz-tag>
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.down' | i18n }} </span><span style="font-weight: bolder">{{ appCountOs.unAvailableSize }}</span>
</nz-tag>
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.paused' | i18n }} </span><span style="font-weight: bolder">{{ appCountOs.unManageSize }}</span>
</nz-tag>
<div style="width: 25%; margin: 3px" ngxSlickItem class="slide">
<div class="hoverCard">
<div nz-row nzAlign="middle" class="bg-primary dashboard-rounded">
<div nz-col nzSpan="9" class="p-md text-white">
<div class="h2 mt0 font-weight-bold">{{ appCountOs.size }}</div>
<p class="h5 text-nowrap mb0">
<i nz-icon nzType="windows" nzTheme="outline"></i>
{{ 'monitor.category.os' | i18n }}
</p>
</div>
<div nz-col nzSpan="15">
<div class="dashboard-slider-tag">
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.up' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountOs.availableSize }}</span>
</nz-tag>
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.down' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountOs.unAvailableSize }}</span>
</nz-tag>
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.paused' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountOs.unManageSize }}</span>
</nz-tag>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div style="width: 25%; margin: 3px" ngxSlickItem class="slide">
<div class="mb-md hoverCard">
<div nz-row nzAlign="middle" class="bg-primary rounded-lg">
<div nz-col nzSpan="9" class="p-md text-white">
<div class="h2 mt0 font-weight-bold">{{ appCountMid.size }}</div>
<p class="h5 text-nowrap mb0">
<i nz-icon nzType="merge-cells" nzTheme="outline"></i>
{{ 'monitor.category.mid' | i18n }}
</p>
</div>
<div nz-col nzSpan="15">
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.up' | i18n }} </span><span style="font-weight: bolder">{{ appCountMid.availableSize }}</span>
</nz-tag>
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.down' | i18n }} </span><span style="font-weight: bolder">{{ appCountMid.unAvailableSize }}</span>
</nz-tag>
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.paused' | i18n }} </span><span style="font-weight: bolder">{{ appCountMid.unManageSize }}</span>
</nz-tag>
<div style="width: 25%; margin: 3px" ngxSlickItem class="slide">
<div class="hoverCard">
<div nz-row nzAlign="middle" class="bg-primary dashboard-rounded">
<div nz-col nzSpan="9" class="p-md text-white">
<div class="h2 mt0 font-weight-bold">{{ appCountMid.size }}</div>
<p class="h5 text-nowrap mb0">
<i nz-icon nzType="merge-cells" nzTheme="outline"></i>
{{ 'monitor.category.mid' | i18n }}
</p>
</div>
<div nz-col nzSpan="15">
<div class="dashboard-slider-tag">
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.up' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountMid.availableSize }}</span>
</nz-tag>
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.down' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountMid.unAvailableSize }}</span>
</nz-tag>
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.paused' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountMid.unManageSize }}</span>
</nz-tag>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div style="width: 25%; margin: 3px" ngxSlickItem class="slide">
<div class="mb-md hoverCard">
<div nz-row nzAlign="middle" class="bg-primary rounded-lg">
<div nz-col nzSpan="9" class="p-md text-white">
<div class="h2 mt0 font-weight-bold">{{ appCountBigdata.size }}</div>
<p class="h5 text-nowrap mb0">
<i nz-icon nzType="dot-chart" nzTheme="outline"></i>
{{ 'monitor.category.bigdata' | i18n }}
</p>
</div>
<div nz-col nzSpan="15">
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.up' | i18n }} </span><span style="font-weight: bolder">{{ appCountBigdata.availableSize }}</span>
</nz-tag>
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.down' | i18n }} </span><span style="font-weight: bolder">{{ appCountBigdata.unAvailableSize }}</span>
</nz-tag>
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.paused' | i18n }} </span><span style="font-weight: bolder">{{ appCountBigdata.unManageSize }}</span>
</nz-tag>
<div style="width: 25%; margin: 3px" ngxSlickItem class="slide">
<div class="hoverCard">
<div nz-row nzAlign="middle" class="bg-primary dashboard-rounded">
<div nz-col nzSpan="9" class="p-md text-white">
<div class="h2 mt0 font-weight-bold">{{ appCountBigdata.size }}</div>
<p class="h5 text-nowrap mb0">
<i nz-icon nzType="dot-chart" nzTheme="outline"></i>
{{ 'monitor.category.bigdata' | i18n }}
</p>
</div>
<div nz-col nzSpan="15">
<div class="dashboard-slider-tag">
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.up' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountBigdata.availableSize }}</span>
</nz-tag>
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.down' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountBigdata.unAvailableSize }}</span>
</nz-tag>
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.paused' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountBigdata.unManageSize }}</span>
</nz-tag>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div style="width: 25%; margin: 3px" ngxSlickItem class="slide">
<div class="mb-md hoverCard">
<div nz-row nzAlign="middle" class="bg-primary rounded-lg">
<div nz-col nzSpan="9" class="p-md text-white">
<div class="h2 mt0 font-weight-bold">{{ appCountWebserver.size }}</div>
<p class="h5 text-nowrap mb0">
<i nz-icon nzType="database" nzTheme="outline"></i>
{{ 'monitor.category.webserver' | i18n }}
</p>
</div>
<div nz-col nzSpan="15">
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.up' | i18n }} </span><span style="font-weight: bolder">{{ appCountWebserver.availableSize }}</span>
</nz-tag>
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.down' | i18n }} </span
><span style="font-weight: bolder">{{ appCountWebserver.unAvailableSize }}</span>
</nz-tag>
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.paused' | i18n }} </span
><span style="font-weight: bolder">{{ appCountWebserver.unManageSize }}</span>
</nz-tag>
<div style="width: 25%; margin: 3px" ngxSlickItem class="slide">
<div class="hoverCard">
<div nz-row nzAlign="middle" class="bg-primary dashboard-rounded">
<div nz-col nzSpan="9" class="p-md text-white">
<div class="h2 mt0 font-weight-bold">{{ appCountWebserver.size }}</div>
<p class="h5 text-nowrap mb0">
<i nz-icon nzType="database" nzTheme="outline"></i>
{{ 'monitor.category.webserver' | i18n }}
</p>
</div>
<div nz-col nzSpan="15">
<div class="dashboard-slider-tag">
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.up' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountWebserver.availableSize }}</span>
</nz-tag>
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.down' | i18n }} </span
><span style="font-weight: bolder">{{ appCountWebserver.unAvailableSize }}</span>
</nz-tag>
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.paused' | i18n }} </span
><span style="font-weight: bolder">{{ appCountWebserver.unManageSize }}</span>
</nz-tag>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div style="width: 25%; margin: 3px" ngxSlickItem class="slide">
<div class="mb-md hoverCard">
<div nz-row nzAlign="middle" class="bg-primary rounded-lg">
<div nz-col nzSpan="9" class="p-md text-white">
<div class="h2 mt0 font-weight-bold">{{ appCountCn.size }}</div>
<p class="h5 text-nowrap mb0">
<i nz-icon nzType="cloud-server" nzTheme="outline"></i>
{{ 'monitor.category.cn' | i18n }}
</p>
</div>
<div nz-col nzSpan="15">
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.up' | i18n }} </span><span style="font-weight: bolder">{{ appCountCn.availableSize }}</span>
</nz-tag>
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.down' | i18n }} </span><span style="font-weight: bolder">{{ appCountCn.unAvailableSize }}</span>
</nz-tag>
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.paused' | i18n }} </span><span style="font-weight: bolder">{{ appCountCn.unManageSize }}</span>
</nz-tag>
<div style="width: 25%; margin: 3px" ngxSlickItem class="slide">
<div class="hoverCard">
<div nz-row nzAlign="middle" class="bg-primary dashboard-rounded">
<div nz-col nzSpan="9" class="p-md text-white">
<div class="h2 mt0 font-weight-bold">{{ appCountCn.size }}</div>
<p class="h5 text-nowrap mb0">
<i nz-icon nzType="cloud-server" nzTheme="outline"></i>
{{ 'monitor.category.cn' | i18n }}
</p>
</div>
<div nz-col nzSpan="15">
<div class="dashboard-slider-tag">
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.up' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountCn.availableSize }}</span>
</nz-tag>
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.down' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountCn.unAvailableSize }}</span>
</nz-tag>
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.paused' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountCn.unManageSize }}</span>
</nz-tag>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div style="width: 25%; margin: 3px" ngxSlickItem class="slide">
<div class="mb-md hoverCard">
<div nz-row nzAlign="middle" class="bg-primary rounded-lg">
<div nz-col nzSpan="9" class="p-md text-white">
<div class="h2 mt0 font-weight-bold">{{ appCountNetwork.size }}</div>
<p class="h5 text-nowrap mb0">
<i nz-icon nzType="global" nzTheme="outline"></i>
{{ 'monitor.category.network' | i18n }}
</p>
</div>
<div nz-col nzSpan="15">
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.up' | i18n }} </span><span style="font-weight: bolder">{{ appCountNetwork.availableSize }}</span>
</nz-tag>
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.down' | i18n }} </span><span style="font-weight: bolder">{{ appCountNetwork.unAvailableSize }}</span>
</nz-tag>
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.paused' | i18n }} </span><span style="font-weight: bolder">{{ appCountNetwork.unManageSize }}</span>
</nz-tag>
<div style="width: 25%; margin: 3px" ngxSlickItem class="slide">
<div class="hoverCard">
<div nz-row nzAlign="middle" class="bg-primary dashboard-rounded">
<div nz-col nzSpan="9" class="p-md text-white">
<div class="h2 mt0 font-weight-bold">{{ appCountNetwork.size }}</div>
<p class="h5 text-nowrap mb0">
<i nz-icon nzType="global" nzTheme="outline"></i>
{{ 'monitor.category.network' | i18n }}
</p>
</div>
<div nz-col nzSpan="15">
<div class="dashboard-slider-tag">
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.up' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountNetwork.availableSize }}</span>
</nz-tag>
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.down' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountNetwork.unAvailableSize }}</span>
</nz-tag>
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.paused' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountNetwork.unManageSize }}</span>
</nz-tag>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div style="width: 25%; margin: 3px" ngxSlickItem class="slide">
<div class="mb-md hoverCard">
<div nz-row nzAlign="middle" class="bg-primary rounded-lg">
<div nz-col nzSpan="9" class="p-md text-white">
<div class="h2 mt0 font-weight-bold">{{ appCountCustom.size }}</div>
<p class="h5 text-nowrap mb0">
<i nz-icon nzType="project" nzTheme="outline"></i>
{{ 'monitor.category.custom' | i18n }}
</p>
</div>
<div nz-col nzSpan="15">
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.up' | i18n }} </span><span style="font-weight: bolder">{{ appCountCustom.availableSize }}</span>
</nz-tag>
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.down' | i18n }} </span><span style="font-weight: bolder">{{ appCountCustom.unAvailableSize }}</span>
</nz-tag>
<nz-tag class="mb-xs">
<span>{{ 'monitor.status.paused' | i18n }} </span><span style="font-weight: bolder">{{ appCountCustom.unManageSize }}</span>
</nz-tag>
<div style="width: 25%; margin: 3px" ngxSlickItem class="slide">
<div class="hoverCard">
<div nz-row nzAlign="middle" class="bg-primary dashboard-rounded">
<div nz-col nzSpan="9" class="p-md text-white">
<div class="h2 mt0 font-weight-bold">{{ appCountCustom.size }}</div>
<p class="h5 text-nowrap mb0">
<i nz-icon nzType="project" nzTheme="outline"></i>
{{ 'monitor.category.custom' | i18n }}
</p>
</div>
<div nz-col nzSpan="15">
<div class="dashboard-slider-tag">
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.up' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountCustom.availableSize }}</span>
</nz-tag>
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.down' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountCustom.unAvailableSize }}</span>
</nz-tag>
<nz-tag [nzBordered]="false" class="mb-xs">
<span>{{ 'monitor.status.paused' | i18n }} </span>
<span style="font-weight: bolder">{{ appCountCustom.unManageSize }}</span>
</nz-tag>
</div>
</div>
</div>
</div>
</div>
</div>
</ngx-slick-carousel>
</div>
</ngx-slick-carousel>
</div>
</div>
<div nz-row nzGutter="16" style="margin-top: 25px; height: 500px">
<div nz-col [nzXs]="{ span: 24 }" [nzSm]="{ span: 24 }" [nzMd]="{ span: 6 }" style="height: 100%">
<nz-spin [nzSpinning]="wordCloudDataLoading">
<angular-tag-cloud
class="br-4"
*ngIf="!wordCloudDataLoading"
(clicked)="onLabelCloudClick($event)"
[font]="'italic bold 6px monospace'"
[data]="wordCloudData"
[width]="1"
[height]="1"
[step]="2"
[strict]="true"
[randomizeAngle]="true"
[realignOnResize]="true"
[delay]="300"
[zoomOnHover]="{ scale: 1.4, transitionTime: 0.6, delay: 0.4 }"
[overflow]="false"
[background]="theme == 'dark' ? '#141414' : 'white'"
>
</angular-tag-cloud>
</nz-spin>
</div>
<div nz-col [nzXs]="{ span: 24 }" [nzSm]="{ span: 24 }" [nzMd]="{ span: 12 }" style="height: 100%">
<nz-spin [nzSpinning]="appsCountLoading">
<div
echarts
[options]="appsCountEChartOption"
theme="default"
[autoResize]="true"
[loading]="appsCountLoading"
(chartClick)="onChartClick($event)"
(chartInit)="onAppsCountChartInit($event)"
style="width: 100%; height: inherit"
></div>
</nz-spin>
</div>
<div nz-col [nzXs]="{ span: 24 }" [nzSm]="{ span: 24 }" [nzMd]="{ span: 6 }" style="height: 100%">
<nz-spin [nzSpinning]="collectorsLoading">
<div style="overflow-y: auto; height: inherit">
<nz-card
*ngFor="let item of collectors; let i = index"
class="rounded-lg collectors"
nzBordered="false"
[nzTitle]="collectorCardTitle"
>
<ng-template #collectorCardTitle>
<span style="font-size: medium; font-weight: bold" nz-tooltip [nzTooltipTitle]="item.collector.name">
<span nz-icon nzType="bug" nzTheme="outline" style="margin-right: 4px"></span>
<span>{{ 'collector' | i18n }} : {{ item.collector.name }}</span>
</span>
</ng-template>
<nz-card-tab>
<nz-tabset nzSize="small" [(nzSelectedIndex)]="collectorsTabSelectedIndex">
<nz-tab [nzTitle]="'collector.status' | i18n" />
<nz-tab [nzTitle]="'collector.task' | i18n" />
<nz-tab [nzTitle]="'collector.start-time' | i18n" />
<nz-tab [nzTitle]="'collector.ip' | i18n" />
<nz-tab [nzTitle]="'collector.node' | i18n" />
</nz-tabset>
</nz-card-tab>
<ng-container *ngIf="collectorsTabSelectedIndex === 0">
<div class="rounded-lg" style="text-align: center; width: 100%; background-color: #8fccca; padding: 2%">
<span [style]="'font-size: x-large; font-weight: bolder;' + (item.collector.status == 0 ? 'color: green' : 'color: red')">
{{ item.collector.status == 0 ? ('monitor.collector.status.online' | i18n) : ('monitor.collector.status.offline' | i18n) }}
</span>
</div>
</ng-container>
<ng-container *ngIf="collectorsTabSelectedIndex === 1">
<div class="rounded-lg" style="text-align: center; width: 100%; background-color: #8fccca; padding: 2%">
<span style="font-size: xxx-large; font-weight: bolder; color: white">
{{ item.pinMonitorNum + item.dispatchMonitorNum }}
</span>
<br />
<nz-tag style="margin-bottom: 4%; font-weight: bolder">{{ 'collector.pinned' | i18n }}: {{ item.pinMonitorNum }}</nz-tag>
<nz-tag style="margin-bottom: 4%; font-weight: bolder"
>{{ 'collector.dispatched' | i18n }}: {{ item.dispatchMonitorNum }}</nz-tag
>
</div>
</ng-container>
<ng-container *ngIf="collectorsTabSelectedIndex === 2">
<div class="rounded-lg" style="text-align: center; width: 100%; background-color: #8fccca; padding: 2%">
<span style="font-size: x-large; font-weight: bolder; color: white">{{
(item.collector.gmtUpdate | date : 'YYYY-MM-dd HH:mm:ss')?.trim()
}}</span>
</div>
</ng-container>
<ng-container *ngIf="collectorsTabSelectedIndex === 3">
<div class="rounded-lg" style="text-align: center; width: 100%; background-color: #8fccca; padding: 2%">
<span style="font-size: x-large; font-weight: bolder; color: white">{{ item.collector.ip }}</span>
</div>
</ng-container>
<ng-container *ngIf="collectorsTabSelectedIndex === 4">
<div class="rounded-lg" style="text-align: center; width: 100%; background-color: #8fccca; padding: 2%">
<span style="font-size: x-large; font-weight: bolder; color: white">{{ item.collector.name }}</span>
</div>
</ng-container>
<div nz-row nzGutter="16" style="margin-top: 20px; height: 500px">
<div nz-col [nzXs]="{ span: 24 }" [nzSm]="{ span: 24 }" [nzMd]="{ span: 6 }" style="height: 100%">
<nz-spin [nzSpinning]="wordCloudDataLoading">
<nz-card class="dashboard-chart-card" style="border-radius: 16px !important; height: 100%; width: 100%">
<angular-tag-cloud
class="br-4"
*ngIf="!wordCloudDataLoading"
(clicked)="onLabelCloudClick($event)"
[font]="'italic bold 6px monospace'"
[data]="wordCloudData"
[width]="1"
[height]="1"
[step]="2"
[strict]="true"
[randomizeAngle]="true"
[realignOnResize]="true"
[delay]="300"
[zoomOnHover]="{ scale: 1.2, transitionTime: 0.6, delay: 0.4 }"
[overflow]="false"
[background]="'transparent'"
>
</angular-tag-cloud>
</nz-card>
</div>
</nz-spin>
</nz-spin>
</div>
<div nz-col [nzXs]="{ span: 24 }" [nzSm]="{ span: 24 }" [nzMd]="{ span: 12 }" style="height: 100%">
<nz-spin [nzSpinning]="appsCountLoading">
<nz-card class="dashboard-chart-card" style="border-radius: 16px !important; height: inherit; overflow: hidden">
<div
echarts
[options]="appsCountEChartOption"
theme="default"
[autoResize]="true"
[loading]="appsCountLoading"
(chartClick)="onChartClick($event)"
(chartInit)="onAppsCountChartInit($event)"
style="width: 100%; height: inherit"
></div>
</nz-card>
</nz-spin>
</div>
<div nz-col [nzXs]="{ span: 24 }" [nzSm]="{ span: 24 }" [nzMd]="{ span: 6 }" style="height: 100%">
<nz-spin [nzSpinning]="collectorsLoading">
<div style="overflow-y: auto; height: inherit">
<nz-card
*ngFor="let item of collectors; let i = index"
class="dashboard-chart-card"
nzBordered="false"
[nzTitle]="collectorCardTitle"
style="border-radius: 16px !important"
>
<ng-template #collectorCardTitle>
<span style="font-size: medium; font-weight: bold" nz-tooltip [nzTooltipTitle]="item.collector.name">
<span nz-icon nzType="bug" nzTheme="outline" style="margin-right: 4px"></span>
<span>{{ 'collector' | i18n }} : {{ item.collector.name }}</span>
</span>
</ng-template>
<nz-card-tab>
<nz-tabset nzSize="small" [(nzSelectedIndex)]="collectorsTabSelectedIndex">
<nz-tab [nzTitle]="'collector.status' | i18n" />
<nz-tab [nzTitle]="'collector.task' | i18n" />
<nz-tab [nzTitle]="'collector.start-time' | i18n" />
<nz-tab [nzTitle]="'collector.ip' | i18n" />
<nz-tab [nzTitle]="'collector.node' | i18n" />
</nz-tabset>
</nz-card-tab>
<ng-container *ngIf="collectorsTabSelectedIndex === 0">
<div class="rounded-lg" style="text-align: center; width: 100%; background-color: transparent; padding: 2%">
<span [style]="'font-size: x-large; font-weight: bolder;' + (item.collector.status == 0 ? 'color: green' : 'color: red')">
{{
item.collector.status == 0 ? ('monitor.collector.status.online' | i18n) : ('monitor.collector.status.offline' | i18n)
}}
</span>
</div>
</ng-container>
<ng-container *ngIf="collectorsTabSelectedIndex === 1">
<div class="rounded-lg" style="text-align: center; width: 100%; background-color: transparent; padding: 2%">
<span style="font-size: xxx-large; font-weight: bolder">
{{ item.pinMonitorNum + item.dispatchMonitorNum }}
</span>
<br />
<nz-tag style="margin-bottom: 4%; font-weight: bolder">{{ 'collector.pinned' | i18n }}: {{ item.pinMonitorNum }}</nz-tag>
<nz-tag style="margin-bottom: 4%; font-weight: bolder"
>{{ 'collector.dispatched' | i18n }}: {{ item.dispatchMonitorNum }}</nz-tag
>
</div>
</ng-container>
<ng-container *ngIf="collectorsTabSelectedIndex === 2">
<div class="rounded-lg" style="text-align: center; width: 100%; background-color: transparent; padding: 2%">
<span style="font-size: x-large; font-weight: bolder">{{
(item.collector.gmtUpdate | date : 'YYYY-MM-dd HH:mm:ss')?.trim()
}}</span>
</div>
</ng-container>
<ng-container *ngIf="collectorsTabSelectedIndex === 3">
<div class="rounded-lg" style="text-align: center; width: 100%; background-color: transparent; padding: 2%">
<span style="font-size: x-large; font-weight: bolder">{{ item.collector.ip }}</span>
</div>
</ng-container>
<ng-container *ngIf="collectorsTabSelectedIndex === 4">
<div class="rounded-lg" style="text-align: center; width: 100%; background-color: transparent; padding: 2%">
<span style="font-size: x-large; font-weight: bolder">{{ item.collector.name }}</span>
</div>
</ng-container>
</nz-card>
</div>
</nz-spin>
</div>
</div>
</div>
<div nz-row nzGutter="16" style="margin-top: 25px; height: 420px">
<div nz-col [nzXs]="{ span: 24 }" [nzSm]="{ span: 24 }" [nzMd]="{ span: 15 }" class="mb-md timeline-card" style="height: 100%">
<nz-spin [nzSpinning]="alertContentLoading">
<nz-card nzHoverable [nzTitle]="alertCardTitleTemplate" [nzExtra]="extraTemplate" style="height: inherit; overflow: hidden">
<nz-timeline nzMode="left">
<nz-timeline-item *ngFor="let alert of alerts; let i = index" [nzLabel]="(alert.activeAt | date : 'YYYY-MM-dd HH:mm:ss')?.trim()">
<p style="font-weight: 400">
<nz-tag *ngIf="alert.labels.severity == 'emergency'" nzColor="red">
<i nz-icon nzType="bell" nzTheme="outline"></i>
<span>{{ 'alert.severity.0' | i18n }}</span>
</nz-tag>
<nz-tag *ngIf="alert.labels.severity == 'critical'" nzColor="orange">
<i nz-icon nzType="bell" nzTheme="outline"></i>
<span>{{ 'alert.severity.1' | i18n }}</span>
</nz-tag>
<nz-tag *ngIf="alert.labels.severity == 'warning'" nzColor="yellow">
<i nz-icon nzType="bell" nzTheme="outline"></i>
<span>{{ 'alert.severity.2' | i18n }}</span>
</nz-tag>
<nz-tag *ngIf="alert.labels.alertname">
<span>{{ alert.labels.alertname }}</span>
</nz-tag>
{{ alert.content }}
</p>
</nz-timeline-item>
</nz-timeline>
<div nz-row nzGutter="16" style="margin-top: 20px; height: 420px">
<div nz-col [nzXs]="{ span: 24 }" [nzSm]="{ span: 24 }" [nzMd]="{ span: 24 }" class="timeline-card" style="height: 100%">
<nz-card
nzHoverable
[nzTitle]="alertCardTitleTemplate"
[nzExtra]="extraTemplate"
class="dashboard-card-alert"
style="border-radius: 16px !important; height: inherit; overflow: hidden"
>
<div nz-row nzGutter="16" style="margin: 0; height: 100%">
<div nz-col [nzXs]="{ span: 24 }" [nzSm]="{ span: 24 }" [nzMd]="{ span: 15 }" class="timeline-card" style="height: 100%">
<nz-spin [nzSpinning]="alertContentLoading">
<nz-timeline nzMode="left">
<nz-timeline-item
*ngFor="let alert of alerts; let i = index"
[nzLabel]="(alert.activeAt | date : 'YYYY-MM-dd HH:mm:ss')?.trim()"
>
<p style="font-weight: 400">
<nz-tag *ngIf="alert.labels.severity == 'emergency'" nzColor="red">
<i nz-icon nzType="bell" nzTheme="outline"></i>
<span>{{ 'alert.severity.0' | i18n }}</span>
</nz-tag>
<nz-tag *ngIf="alert.labels.severity == 'critical'" nzColor="orange">
<i nz-icon nzType="bell" nzTheme="outline"></i>
<span>{{ 'alert.severity.1' | i18n }}</span>
</nz-tag>
<nz-tag *ngIf="alert.labels.severity == 'warning'" nzColor="yellow">
<i nz-icon nzType="bell" nzTheme="outline"></i>
<span>{{ 'alert.severity.2' | i18n }}</span>
</nz-tag>
<nz-tag *ngIf="alert.labels.alertname">
<span>{{ alert.labels.alertname }}</span>
</nz-tag>
{{ alert.content }}
</p>
</nz-timeline-item>
</nz-timeline>
</nz-spin>
</div>
<div nz-col [nzXs]="{ span: 24 }" [nzSm]="{ span: 24 }" [nzMd]="{ span: 9 }" class="timeline-card" style="height: 100%">
<nz-spin [nzSpinning]="alertsLoading">
<div
echarts
[options]="alertsEChartOption"
theme="default"
[autoResize]="true"
[loading]="alertsLoading"
(chartInit)="onAlertNumChartInit($event)"
style="width: 100%; height: inherit"
></div>
</nz-spin>
</div>
</div>
</nz-card>
</nz-spin>
</div>
<div nz-col [nzXs]="{ span: 24 }" [nzSm]="{ span: 12 }" [nzMd]="{ span: 9 }" class="mb-md" style="height: 100%">
<nz-spin [nzSpinning]="alertsLoading">
<div
echarts
[options]="alertsEChartOption"
theme="default"
[autoResize]="true"
[loading]="alertsLoading"
(chartInit)="onAlertNumChartInit($event)"
style="width: 100%; height: inherit; padding-left: 30px"
class="ant-card ant-card-bordered ant-card-hoverable"
></div>
</nz-spin>
</div>
</div>
<ng-template #extraTemplate>
<a [routerLink]="['/alert/center']" style="font-weight: bold">{{ 'dashboard.alerts.enter' | i18n }}</a>
</ng-template>
<ng-template #alertCardTitleTemplate>
<span style="font-size: 14px; font-weight: bold">{{ 'dashboard.alerts.title' | i18n }}</span>
</ng-template>
</div>
<ng-template #extraTemplate>
<a [routerLink]="['/alert/center']" style="font-weight: bold">{{ 'dashboard.alerts.enter' | i18n }}</a>
</ng-template>
<ng-template #alertCardTitleTemplate>
<span style="font-size: 14px; font-weight: bold">{{ 'dashboard.alerts.title' | i18n }}</span>
</ng-template>

Some files were not shown because too many files have changed in this diff Show More