[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>
This commit is contained in:
Sarthak Arora
2025-09-10 08:31:05 +08:00
committed by GitHub
co-authored by Calvin Jast Duansg DeleiGuo shown tomsun28 Logic Copilot Sherlock Yin lynx009
parent 7d8b55dfb8
commit 05b63903b5
29 changed files with 3448 additions and 248 deletions
+4
View File
@@ -57,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);
}
@@ -67,5 +67,5 @@ public interface MonitorServiceAdapter {
* @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);
}
}
}
@@ -211,5 +211,4 @@ public class MonitorServiceAdapterImpl implements MonitorServiceAdapter {
throw new RuntimeException("Failed to invoke getAppParamDefines via adapter: " + e.getMessage(), e);
}
}
}
@@ -22,6 +22,7 @@ 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;
@@ -32,6 +33,9 @@ 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
*/
@@ -48,7 +52,7 @@ public class LlmConfig {
@Bean
public OpenAiChatOptions openAiChatOptions() {
return OpenAiChatOptions.builder()
.model("gpt-4.1-nano-2025-04-14")
.model(model)
.temperature(0.3)
.build();
}
@@ -29,49 +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.
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 and analyze their monitoring data using the available tools.
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 Monitoring Tools:
- **list_monitors**: Query monitor information with detailed output (ID, name, type, host, status)
- **add_monitor**: Add a new monitor to the system with comprehensive configuration
- **list_monitor_types**: List all available monitor types (linux, mysql, redis, http, etc.)
- **get_monitor_param_defines**: Get parameter definitions required for specific monitor types
## 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?"
## HertzBeat Monitor Types:
HertzBeat supports monitoring of:
- **Operating Systems**: Linux, Windows, FreeBSD, macOS, etc.
- **Databases**: MySQL, PostgreSQL, Redis, MongoDB, Oracle, SQL Server, etc.
- **Application Services**: Tomcat, Spring Boot, Elasticsearch, Kafka, etc.
- **Network & Infrastructure**: HTTP/HTTPS websites, DNS, ping, SSL certificates, etc.
- **Cloud Services**: AWS, Azure, Kubernetes, Docker, etc.
- **Custom Monitoring**: Through YAML templates and various protocols (HTTP, JDBC, SSH, JMX, SNMP, etc.)
## Workflow Guidelines:
1. **For viewing monitors**: Use list_monitors with appropriate filters (by type, status, host, etc.)
2. **For adding monitors**:
- First use list_monitor_types to show available types
- Then use get_monitor_param_defines to show required parameters for the chosen type
- Finally use add_monitor with all necessary parameters
## Parameter Values:
- **Monitor status**: 0 (no monitor), 1 (usable), 2 (disabled), 9 (all)
- **Sort fields**: name, host, app, gmtCreate, gmtUpdate
- **Sort order**: 'asc' or 'desc'
- **Monitor intervals**: Typically 30s to 3600s (30 seconds to 1 hour)
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:
- Always validate monitor types using list_monitor_types before adding
- Check parameter requirements using get_monitor_param_defines for each monitor type
- Provide clear explanations of monitoring data and suggest actionable insights
- For performance issues, recommend appropriate collection intervals
- Explain HertzBeat's template-based YAML monitoring definitions when relevant
- 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
Keep responses focused on monitoring topics and HertzBeat's comprehensive monitoring capabilities.
If you're unsure about specific monitoring requirements, use the parameter definition tools to get
accurate information.
## 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.
""";
}
@@ -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;
}
@@ -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);
}
@@ -17,6 +17,7 @@
package org.apache.hertzbeat.ai.agent.service;
import org.apache.hertzbeat.ai.agent.event.OpenAiConfigChangeEvent;
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
/**
@@ -62,6 +63,12 @@ public interface OpenAiConfigService {
*/
void reloadConfig();
/**
* Handle OpenAI configuration change events
* @param event OpenAI configuration change event
*/
void onOpenAiConfigChange(OpenAiConfigChangeEvent event);
/**
* Validation result class
*/
@@ -85,11 +85,10 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
return this.chatClient.prompt()
.messages(messages)
.system(PromptProvider.HERTZBEAT_MONITORING_PROMPT)
.system(PromptProvider.HERTZBEAT_SYSTEM_PROMPT)
.toolCallbacks(toolCallbackProvider)
.stream()
.content()
.doOnNext(chunk -> log.debug("Received chunk: {}", chunk))
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", context.getConversationId()))
.doOnError(error -> log.error("Error in streaming chat: {}", error.getMessage(), error));
@@ -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,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();
}
@@ -27,7 +27,7 @@ public interface MonitorTools {
/**
* 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)
@@ -35,35 +35,47 @@ public interface MonitorTools {
* @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 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 string for AI tool.
* 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,
@@ -72,15 +84,14 @@ public interface MonitorTools {
String sort,
String order,
Integer pageIndex,
Integer pageSize);
Integer pageSize,
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 getMonitorParamDefines(String app);
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,6 +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.apache.hertzbeat.ai.agent.utils.UtilityClass;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
@@ -34,7 +35,6 @@ import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Implementation of Monitoring Tools functionality
@@ -42,7 +42,6 @@ import java.util.stream.Collectors;
@Slf4j
@Service
public class MonitorToolsImpl implements MonitorTools {
@Autowired
private MonitorServiceAdapter monitorServiceAdapter;
@@ -54,45 +53,132 @@ public class MonitorToolsImpl implements MonitorTools {
* Returns detailed monitor information including ID, name, type, host, and status.
*/
@Override
@Tool(name = "list_monitors", description = """
Query monitor information with flexible filtering and pagination.
Supports filtering by monitor IDs, type, status, host, labels, sorting, and pagination.
Returns detailed results including monitor ID, name, type, host, and status for easy identification and management.
Show the long monitor id in the brackets
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) {
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);
// Format response to include both ID and name for better usability
StringBuilder response = new StringBuilder();
response.append("Found ").append(result.getContent().size()).append(" monitors:\n\n");
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()) {
log.info(String.valueOf(monitor.getId()));
response.append("ID: ").append(monitor.getId())
.append(" | Name: ").append(monitor.getName())
.append(" | Type: ").append(monitor.getApp())
.append(" | Host: ").append(monitor.getHost())
.append(" | Status: ").append(getStatusText(monitor.getStatus()))
.append("\n");
.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.");
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 retrieving monitors: " + e.getMessage();
@@ -102,26 +188,53 @@ public class MonitorToolsImpl implements MonitorTools {
@Override
@Tool(name = "add_monitor", description = """
Add a new monitor to HertzBeat with comprehensive configuration.
This tool creates a monitor for various types like linux, mysql, http, redis, etc.
validate the the monitor type using the list_monitor_types tool.
ALWAYS Ask the user to give all the the parameter definitions for the asked monitor type using the get_monitor_param_defines tool.
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/application: linux, mysql, http, redis, postgresql, etc.", required = true) String app,
@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, depends on monitor type)", required = false) Integer port,
@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);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in add_monitor tool: {}", subjectSum);
// Validate required parameters
if (name == null || name.trim().isEmpty()) {
return "Error: Monitor name is required";
@@ -132,239 +245,247 @@ public class MonitorToolsImpl implements MonitorTools {
if (host == null || host.trim().isEmpty()) {
return "Error: Host is required";
}
// Set default values
// Set defaults
if (intervals == null || intervals < 10) {
intervals = 600; // Default 10 minutes
intervals = 600;
}
// Create Monitor entity
Monitor monitor = Monitor.builder()
.name(name.trim())
.app(app.toLowerCase().trim())
.host(host.trim())
.intervals(intervals)
.status((byte) 1) // Status: Up
.type((byte) 0) // Type: Normal
.status((byte) 1)
.type((byte) 0)
.description(description != null ? description.trim() : "")
.build();
// Create parameters list
List<Param> params = new ArrayList<>();
// Add host parameter (always required)
params.add(Param.builder()
.field("host")
.paramValue(host.trim())
.type((byte) 0)
.build());
// Add port parameter if provided
if (port != null && port > 0) {
params.add(Param.builder()
.field("port")
.paramValue(port.toString())
.type((byte) 0)
.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());
}
// Add authentication parameters if provided
if (username != null && !username.trim().isEmpty()) {
params.add(Param.builder()
.field("username")
.paramValue(username.trim())
.type((byte) 1) // Type: Password
.build());
}
if (password != null && !password.trim().isEmpty()) {
params.add(Param.builder()
.field("password")
.paramValue(password.trim())
.type((byte) 1) // Type: Password
.build());
}
// Add timeout parameter (default)
params.add(Param.builder()
.field("timeout")
.paramValue("6000")
.type((byte) 0)
.build());
// Call the adapter to add the monitor
// 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 monitor '%s' with ID: %d. Monitor type: %s, Host: %s, Interval: %d seconds",
name, monitorId, app, host, intervals);
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 HerzBeat.
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())
.collect(Collectors.toList());
.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");
.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.");
response.append("\nExample: add_monitor(name='my-server', app='linux', host='192.168.1.100')");
log.info("Successfully listed {} monitor types", monitorTypes.size());
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_param_defines", description = """
@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,
including field names, data types, validation rules, and whether they are required.
Use this before adding a monitor to understand what parameters you need to provide.
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 getMonitorParamDefines(
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);
+ "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()));
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();
}
}
/**
* Helper method to convert monitor status byte to readable text
* @param status The status byte from monitor
* @return Human-readable status text
*/
private String getStatusText(Byte status) {
if (status == null) {
return "Unknown";
}
switch (status) {
case 0:
return "Paused";
case 1:
return "Online";
case 2:
return "Offline";
case 3:
return "Unreachable";
default:
return "Unknown (" + status + ")";
}
}
}
}
@@ -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;
}
}
+131 -14
View File
@@ -2,25 +2,47 @@
id: ai_agent_chat
title: AI Agent Chat User Guide
sidebar_label: AI Agent Chat
keywords: [AI, Chat, Agent, Monitoring, Assistant, OpenAI]
keywords: [AI, Chat, Agent, Monitoring, AI Agent, OpenAI]
---
> HertzBeat AI Agent Chat is an intelligent monitoring assistant that helps you manage monitors, configure alerts, and optimize your infrastructure monitoring through natural language conversation.
> 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:
- 🔍 List and manage your existing monitors
**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
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
@@ -58,7 +80,7 @@ spring:
### Accessing the Chat Interface
1. Navigate to the **AI Agent Chat** section in HertzBeat web interface
2. You'll see the HertzBeat Monitoring Assistant welcome screen
2. You'll see the HertzBeat Monitoring AI Agent welcome screen
3. Start typing your questions in the chat input field
### Sample Conversations
@@ -67,7 +89,7 @@ spring:
```text
You: Add a new HTTP monitor for my website https://example.com
Assistant: I'll help you add an HTTP monitor for 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
@@ -83,7 +105,7 @@ Monitor created successfully with ID: 12345
```text
You: Show me all my current monitors
Assistant: Here are your current monitors:
AI Agent: Here are your current monitors:
1. **Linux Server** (ID: 1001)
- Status: ✅ Online
@@ -107,7 +129,7 @@ Would you like details on any specific monitor?
```text
You: What parameters do I need to monitor a PostgreSQL database?
Assistant: To monitor a PostgreSQL database, you'll need these parameters:
AI Agent: To monitor a PostgreSQL database, you'll need these parameters:
**Required Parameters:**
- **Host**: Database server IP/hostname
@@ -124,6 +146,107 @@ Assistant: To monitor a PostgreSQL database, you'll need these parameters:
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
@@ -228,12 +351,6 @@ You: Add HTTP monitors for all services in my staging environment:
- admin-staging.example.com:3000
```
### Integration Suggestions
```text
You: What's the best way to monitor a microservices architecture with 20+ services?
```
## Limitations
- Requires active internet connection for OpenAI API
+25 -4
View File
@@ -63,11 +63,32 @@ After saving, reload MCP in Cursor or restart the editor.
### Tools available
- **list_monitors**: Query monitor information with flexible filtering and pagination. Supports filtering by monitor IDs, type, status, host, labels, sorting, and pagination.
- **add_monitor**: Add a new monitor to HertzBeat with comprehensive configuration. Creates monitors for various types like linux, mysql, http, redis, etc.
- **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_param_defines**: Get the parameter definitions required for a specific monitor type. Shows what parameters are needed when adding a monitor.
#### Monitor Management Tools
- **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
@@ -366,23 +366,25 @@ global-footer {
border-radius: 16px !important;
overflow: hidden !important;
}
.ant-modal-content {
border-radius: 16px !important;
overflow: hidden !important;
}
.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;
}
.ant-modal-close {
top: 8px !important;
right: 8px !important;
@@ -88,10 +88,12 @@
to:
</p>
<ul>
<li>🔍 List and manage your existing monitors</li>
<li>🔍 Query and manage monitors by status, type, or labels</li>
<li> Add new monitors for websites, APIs, databases, and services</li>
<li>📊 Get detailed information about available monitor types and their parameters</li>
<li>⚡ Check monitor status and troubleshoot monitoring issues</li>
<li>🚨 View and analyze alerts with comprehensive filtering</li>
<li>📊 Access real-time and historical metrics data</li>
<li>⚙️ Configure alert rules and thresholds</li>
<li>⚡ Troubleshoot performance issues across your infrastructure</li>
</ul>
<p>Ask me anything about your monitoring setup!</p>
</div>